Functional Programming Part 1: Purity and Immutability

Lecture 16 min.



Analogies for learning functional programming

When we were first learning to drive, we tried as hard as we could. Sure, it looked easy when we watched other people drive. But in practice it turned out to be much harder.

We practiced on our parents' car and stayed off the highways until we had fully mastered the streets of our own neighborhood.

After a lot of practice and a few close calls our parents would probably rather forget, we learned to drive and finally got our driver's license.

License in hand, we got behind the wheel at every opportunity. With every new trip our skills got better and better, and our confidence grew. Then came the day when we had to drive a different car, or our own car gave up the ghost and we bought a new one.

Remember what the first time behind the wheel of a different car felt like? Was it anything like the very first time driving at all? Not even close. The first time, everything was unfamiliar. Sure, we'd sat in a car before, but only as a passenger. This time we were in the driver's seat. Alone with all the levers, buttons and pedals.

But when we drove our second car, we just asked ourselves a few simple questions, like: where does the key go, where do you switch between low and high beams, how do you use the turn signals, and how do you adjust the mirrors.

After all that, we drove our car like clockwork. But why was it so much easier this time compared to the first?

Because the new car was similar enough to the old one. It had all the basic elements a car needs, and in most cases they were in the same places as in the old one.

A few things were implemented a bit differently, and maybe it had some extra features, but we hadn't used those anyway during our entire driving experience. Sooner or later we'd learn all the new gadgets. At least the ones we actually needed.

Learning programming languages is a lot like learning to drive. The first time is the hardest. But with a bit of experience under your belt, every subsequent time gets easier.

When you start learning a second language, you ask yourself: "How do I create a module? How do I implement a search over an array? What parameters does the substring-search function take?".

You're confident you can "learn to drive" this new language, because it reminds you of the previous one, maybe with a few new elements that will hopefully make your life easier.

Going deeper

Whether you've driven one car your whole life or dozens of cars, imagine you're about to sit down at the controls of a spaceship.

If you're going to fly a craft like that, you probably shouldn't expect your driving skills on the road to help you much. You'll start from zero (We're programmers, after all. We start counting from zero.).

You'll start your training assuming that everything works differently in space and that flying this thing is quite different from driving a car on the ground.

However, the physics hasn't changed. The path you travel is still within the same Universe.

You should take the same approach when learning functional programming. You should expect everything to be different. And that much of what you knew about programming won't carry over into this new field.

Programming means thinking, and functional programming will teach you to think in a completely different way — so different that you'll probably never go back to your old way of thinking.

Forget everything you know

Functional Programming Part 1: Purity and Immutability

People love to say this phrase, and there really is some truth to it. Learning functional programming means learning everything from scratch. Not entirely, of course, but essentially that's how it is. This subject has many simple concepts, but you'd better be prepared to relearn everything.

With the right mindset, you'll have the right expectations, and with the right expectations, you won't want to give up when things start getting harder.

There are also many things you're used to doing as a programmer that you won't be able to do anymore once you start doing functional programming.

Remember how, to back out of a parking spot, you'd shift the car into reverse. But a spaceship has no reverse gear. Now you have to think: "WHAT? NO REVERSE? HOW AM I SUPPOSED TO DRIVE WITHOUT REVERSE?".

Well, it turns out you don't need reverse on a spaceship capable of maneuvering in three-dimensional space. Once you understand that, you'll stop thinking about reverse altogether. And one day, you might even wonder just how limited ordinary cars really are.

Learning functional programming takes time. Be patient.

So let's leave the cold world of imperative programming behind and slowly dip into the hot springs of functional programming.

What follows in this comprehensive article are the concepts of functional programming that will prepare you before you fully dive into your first functional language. Or, if you've already taken the plunge into this field, these paragraphs will help sharpen your understanding of the ideas.

Please, don't rush. From this point on, take your time and find the moments to understand the code examples. It might even help to take a small break from reading after this part of the article and let the ideas you've encountered settle in. Then come back and keep reading.

The most important thing is that you understand.

Function purity

Functional Programming Part 1: Purity and Immutability

In programming languages, a pure function is a function that:

  1. is deterministic;
  2. has no side effects.

Having only one of these properties isn't enough for a function to be pure.

Function determinism

Nondeterminism of a function is the possibility of the function returning different values even though it's given the same input argument values. In this case, it's impossible to build a unique table of the function's values; for such functions, the value tables look like a list (possibly infinite) of the possible values the function takes for a given set of input parameters.

A function is deterministic if, for the same set of input values, it always returns the same result.

Function side effects

In imperative languages, some functions may, while performing their computations, modify the values of global variables, perform input/output operations, or react to exceptional situations by invoking their handlers. Such functions are called functions with side effects. Another kind of side effect is modifying the parameters (variables) passed into the function, when the value of the input parameter itself is changed while computing the function's output value.

Almost any programming language lets you write functions without side effects. However, some languages encourage or even require certain kinds of functions to use side effects. For example, in many object-oriented languages, a class member function is passed a hidden parameter — a pointer to the instance of the class on whose behalf the corresponding function is called (for example, in C++ this parameter is called this, and in Object Pascal — self), which the function implicitly modifies. Nevertheless, in C++ you can mark a class method with the const modifier, thereby telling the compiler that the method doesn't modify the class's data.

Orthogonality of determinism and side effects

Usually, functions that have side effects are not deterministic, which is why functions without side effects, deterministic functions, and pure functions are sometimes confused with one another. In reality these are different properties of functions. For example, the function rand, which returns a random number, or the hypothetical function GetGlobalVarX, which returns the value of the global variable X (and does nothing else), are not deterministic, even though they have no side effects. On the other hand, the hypothetical function print, which prints text to the screen and always returns 0, is deterministic but does have a side effect (printing text to the screen). Neither of them is pure.

When people talk about purity in functional programming, they mean pure functions.

Pure functions are very simple. All they do is perform an operation on their input data.

Here's an example of a pure function:

var z = 10;
function add(x, y) {
    return x + y;
}

Notice that the function add never touches the variable z. It doesn't read its value and doesn't write anything to it. The function only reads x and y, its input data, and returns the result of their sum.

This is a pure function. If the function add had access to the variable z, it could no longer be pure.

Here's an example of another pure function:

function justTen() {
    return 10;
}

If the function justTen is pure, it can only ever return a constant value. Why?

Because we don't give it any input data. Which means that, in order to be pure, it must not change any variables other than the ones passed to it. The only thing such a function can return is a constant.

While functions that take no parameters do work, they're not very useful. It would be better to simply declare justTen as a constant.

More useful pure functions take at least one parameter.

Take a look at this example:

function addNoReturn(x, y) {
    var z = x + y
}

Notice that this function doesn't return anything. It adds x and y, stores the result in the variable z, but never returns it.

This pure function only works with its input data. Yes, it performs the addition, but since nothing is returned, the function is useless.

All useful pure functions must return something.

Let's look at the first add function example again:

function add(x, y) {
    return x + y;
}
console.log(add(1, 2)); // prints 3
console.log(add(1, 2)); // still prints 3
console.log(add(1, 2)); // WILL ALWAYS print 3

Notice that add(1, 2) always results in 3. Sure, that's not much of a surprise, but that's precisely because the function is pure. If the add function took a value from somewhere outside itself, you could never reliably predict its behavior.

A pure function always returns the same values for the same input data.

Since pure functions can't change external variables, all of the following functions are impure:

writeFile(fileName);
updateDatabaseTable(sqlCmd);
sendAjaxRequest(ajaxRequest);
openSocket(ipAddress);

All the functions in the example have what's called side effects. When you call them, they change files and database tables, send data to a server, or talk to the operating system to obtain a socket. They do far more than simply operating on input data and returning a value. As a result, you can never predict what such a function will return.

Pure functions have no side effects.

In imperative programming languages like JavaScript, Java and C#, side effects are everywhere. This makes debugging problematic, because a variable in your program's code can be changed anywhere. In general, if you have a bug caused by a variable getting the wrong value at the wrong time, where would you look for the error? Everywhere? That won't work.

At this point, you're probably thinking: "HOW ON EARTH AM I SUPPOSED TO DO ANYTHING WITH ONLY PURE FUNCTIONS?".

In functional programming, you don't write only pure functions.

Functional languages can't eliminate side effects, they can only isolate them. As long as programs have interfaces that interact with the real world, some parts of any program must be impure. The goal is to minimize the amount of impure code and separate it from the rest of the program.

Immutability of values

Do you remember the first time you saw code like this:

var x = 1;
x = x + 1;

And whoever taught you to program told you to forget what you learned in math class. Because in math, x could never equal x + 1.

But in imperative programming, this code means "take the current value of x, add 1 to it, and put the result back into x".

Well, in functional programming, the expression x = x + 1 is not allowed. So you need to remember what you forgot from math class... so to speak.

In functional programming, there are no variables.

Stored values are still called variables for historical reasons, but they are actually constants, meaning that x, once it takes on a value, keeps that value for its whole lifetime.

Don't worry, x is usually a local variable, so its lifetime is fairly short. But for as long as it's alive, it never changes.

Here's an example of a constant variable in Elm — a pure functional programming language for web development:

addOneToSum y z =
    let
        x = 1
    in
        x + y + z

If you're not familiar with the syntax of the ML family of programming languages, let me explain. addOneToSum is a function that takes 2 parameters: y and z.

Inside the let block, x is assigned the value 1, meaning it equals 1 for the rest of its lifetime. Its lifetime ends when the function exits, or more precisely, when the let block finishes executing.

Inside the in block, the computations can include values declared in the let block, namely x. The result of computing x + y + z is returned, or more precisely, 1 + y + z is returned, since x = 1.

And again I can hear you asking: "HOW ON EARTH AM I SUPPOSED TO DO ANYTHING WITHOUT VARIABLES?!".

Let's think about when we usually want to change a variable. There are essentially two main reasons that come to mind: multi-value changes (for example, changing a single value of an object or record) and single-value changes (for example, loop counters).

Functional programming solves the problem of changing a record's value by making a copy of the already-modified record. This happens efficiently, without copying every part of the record, by using specific data structures that make it possible.

Functional programming also solves the problem of single-value variable changes essentially the same way, simply by making a copy of them.

And by the way, all of this happens without loops.

"FIRST NO VARIABLES, AND NOW NO LOOPS EITHER? I HATE YOU!!!"

Hold your horses. This doesn't mean we can't use loops, it just means there are no dedicated operators like for, while, do, repeat, and so on.

Functional programming uses recursion to perform looping.

Here are two examples of implementing a loop in JavaScript.

// a simple loop statement
var acc = 0;
for (var i = 1; i <= 10; ++i)
    acc += i;
console.log(acc); // prints 55// without a loop statement or variables (recursion)
function sumRange(start, end, acc) {
    if (start > end)
        return acc;
    return sumRange(start + 1, end, acc + start)
}
console.log(sumRange(1, 10, 0)); // prints 55

Notice how the recursive approach in the functional style does the same thing as a for loop, by calling itself with a new starting parameter (start + 1) and a new counter (acc + start). It doesn't modify the old values. Instead, it uses new values computed from the old ones.

Unfortunately, examples like this aren't very intuitive in JavaScript (even if you spend some time studying them) for two reasons. First, JavaScript's syntax is cluttered, and second, you're probably not used to thinking recursively.

The example in Elm is easier to read and, as a result, easier to understand:

sumRange start end acc =
    if start > end then
        acc
    else
        sumRange (start + 1) end (acc + start)

Here's how this code executes:

sumRange 1 10 0 =      -- sumRange (1 + 1)  10 (0 + 1)
sumRange 2 10 1 =      -- sumRange (2 + 1)  10 (1 + 2)
sumRange 3 10 3 =      -- sumRange (3 + 1)  10 (3 + 3)
sumRange 4 10 6 =      -- sumRange (4 + 1)  10 (6 + 4)
sumRange 5 10 10 =     -- sumRange (5 + 1)  10 (10 + 5)
sumRange 6 10 15 =     -- sumRange (6 + 1)  10 (15 + 6)
sumRange 7 10 21 =     -- sumRange (7 + 1)  10 (21 + 7)
sumRange 8 10 28 =     -- sumRange (8 + 1)  10 (28 + 8)
sumRange 9 10 36 =     -- sumRange (9 + 1)  10 (36 + 9)
sumRange 10 10 45 =    -- sumRange (10 + 1) 10 (45 + 10)
sumRange 11 10 55 =    -- 11 > 10 => 55
55

You probably feel that for loops are much easier to understand. While that's debatable and is likely mostly a matter of familiarity, non-recursive loops imply mutability, which is inherently bad.

I won't explain the benefits of the immutability paradigm here, but you can check out the section called Global Mutable State in the article Why Programmers Need Limits if you want to dig into this topic.

One obvious benefit is that if you have access to some value in your program, that access is read-only, meaning no one else can change that value. Not even you. As a result, no accidental changes.

Also, if the program is multithreaded, no thread's execution can wreck your plans. Since the value is a constant, if a thread wants to change it, it has to create a new value from the old one.

Back in the mid-nineties I wrote a game engine for Creator Crunch, and the biggest source of bugs was related to multithreading issues. I wish I'd known about immutability back then. But at the time I was more concerned with the difference between double-speed and quad-speed CD-ROM drives while gaming.

Immutability makes code simpler and safer.

See also

  • Side effect (computer science)
  • [[b8519]]

See also

Comments

To leave a comment

If you have any suggestion, idea, thanks or comment, feel free to write. We really value feedback and are glad to hear your opinion.
To reply

Lectures and tutorial on "Functional programming"

Terms: Functional programming