Immutability is Awesome

Immutability is a confusing concept in Haskell. It is the property of all Haskell expressions that, once they are assigned to a name, they CANNOT change their value. To most programmers, this seems like a completely crazy idea. How can you program at all when you cannot change the value of an expression? To start, let’s consider this example of how mutability works in Python:

>> a = [1,2,3]
>> a.reverse()
>> a
[3, 2, 1]
>> b = a.reverse()
>> a
[1, 2, 3]
>> b
(No output)
>>

We see now when we call the reverse function on a, the value of a actually changes. We call a later, and we get a reversed list. Meanwhile, reverse has no return value. When we assign the result of a.reverse() to b, we get nothing.

Let’s see what happens in Haskell:

>> let a = [1,2,3]
>> reverse a
[3,2,1]
>> a
[1,2,3]

This time, the value of a did not change. Calling reverse seems to have produced the output we expected, but it had no impact on the original variable. To access the reversed list, we would need to assign it to a name (by using let).

Build it Again

To understand what it is happening, let’s first look at the type signature for reverse:

reverse :: [a] -> [a]

We see reverse takes one argument, a list. It then outputs a list of the same type. It’s a pure function, so it cannot change the input list! Instead, it constructs a completely new list with the same elements as the input, only in reverse order. This may seem strange, but it has huge implications on our ability to reason about code. Consider the following:

operateOnList :: [Int] -> [Int]
operateOnList ints = concat [l1, l2, l3]
  where 
    l1 = funcOnList1 ints
    l2 = funcOnList2 ints
    l3 = funcOnList3 ints

Comparable python code is much harder to reason about. Each function might mutate the input list, so we don’t know what value is passed to the second and third functions. However, in Haskell, we know we will pass the exact same list as a parameter to each of these functions! This simple principle makes it extremely easy to refactor Haskell code.

Efficiency Concerns

You might be wondering, isn’t immutability really inefficient? If reverse creates a completely new list, is it copying every element from the old list? In fact, it does not copy the elements. It simply uses the exact same elements from the original list. In C++ or Python, this would raise the question of what would happen when we modify one of the elements in the first list. Wouldn’t the reversed list also change? But in Haskell, we can’t modify the items in the first list because of immutability! So we’re safe!

Now, remaking the data structure itself does introduce some overhead. In the case of reverse we do have to create a new linked list with a new set of pointers. But in most cases, it’s not so bad. Consider the concatenation operator.

(:) :: a -> [a] -> [a]

This function takes an element and a list and returns a new list with the new element appended to the front of the old list. This is quite efficient, because the new list simply uses the old list as its tail. There is no need to re-create a separate data structure. Immutability protects us from all the nasty side effects of having two data structures operate on the same data. Other data structures work in the same way. For instance, the insert function of the Map data structure takes a key, a value, and an old map, and returns a new map with all the elements of the old map, plus the new mapping between the key and the value.

insert :: k -> v -> Map k v -> Map k v

This is done efficiently, without having to recreate any of the structure of the old map or any of its underlying elements. In a previous article we discussed record syntax on algebraic data types. We saw how we could “modify” objects with record syntax:

makeJohnDoe :: Person -> Person
makeJohnDoe person = person
  { firstName = “John”
  , lastName = “Doe” }

But this does not change the old Person object. This has the same effect as reversing a list. It recreates the Person structure, but none of the underlying elements. And the result is a completely new Person object. So remember:

Summary

  1. Expressions in Haskell are immutable. They cannot change after they are evaluated.
  2. Immutability makes refactoring super easy and code much easier to reason about.
  3. To “change” an object, most data structures provide methods taking the old object and creating a new copy.

If you want to get started with Haskell, be sure to check out our checklist to make sure you have everything you need!

Previous
Previous

Faster Code with Laziness

Next
Next

Many Types, One Behavior