Immutability: The Less Things Change, The More You Know
Most programmers take it for granted that they can change the values of their expressions. Consider this simple Python example:
> a = [1,2,3]
> a.reverse()
> a
[3,2,1]
We can see that the reverse
function actually changed the underlying list. Here’s another example, this time in C++. We pass a pointer to an integer as a parameter, and we can update the integer within the function.
int myFunction(int* a) {
int result = 0;
if (*a % 2 == 0) {
result = 10;
} else {
result = 20;
}
++(*a);
return result;
}
When we call this function, the original expression changes values.
int main() {
int x = 4;
int* xp = &x;
int result = myFunction(xp);
cout << result << endl;
cout << *xp << endl;
}
Even though xp
initially points to the value 4, when we print it at the end, the value is now 5! But as we’ll learn, Haskell does not, in general, allow us to do this! Let’s see how it works.
Immutability
In Haskell, all expressions are immutable! This means you cannot change the underlying value of something like you can in Python or C++. There are still some functions that appear to mutate things. But in general, they don’t change the original value. They create entirely new values! Let’s look at an example with reverse:
>> let a = [1,2,3]
>> reverse a
[3,2,1]
>> a
[1,2,3] -- unchanged!
The reverse
function takes one argument, a list, and returns a list. But the final value is a totally new list! Observe how the original expression a
remains the same! Compare this to the earlier Python example. The reverse
function actually had a “void” return value. Instead, it changed the original list.
Record syntax is another example where we appear to mutate a value in Haskell. Consider this type and an accompanying mutator function:
data Person = Person
{ personName :: String
, personAge :: Int
} deriving (Show)
makeAdult :: Person -> Person
makeAdult person = person { personAge = 18}
But when we actually use the function, we’ll find again that it creates a totally new value! The old one stays the same!
>> let p = Person “John” 17
>> makeAdult p
Person {personName = “John”, personAge = 18}
>> p
Person {personName = “John”, personAge = 17}
Advantages of Immutability
Immutability might seem constraining at first. But it’s actually very liberating! Until you try programming with immutability by default, you don’t realize quite how many bugs mutable data causes. It is tremendously useful to know that your values cannot change. Suppose we take a list as a parameter to a function. In Haskell, we know that no matter how many functions we call with that list as a parameter, it will still be the same each time.
example :: [Int] -> Int
example myList = …
where
-- Each call uses the EXACT same list!
result1 = function1 myList
result2 = function2 result1 myList
result3 = function3 result2 myList
Immutability also means you don’t have to worry about different ways to “copy” a data structure. Every copy is a shallow copy, since you can’t change the values within the structure anyway!
Ways Around Immutability
Naturally, there are situations where you want to have mutable data. But we can always simulate this effect in Haskell by using more advanced types! For instance, we can easily represent the C++ function above using the State
monad.
myFunction :: State Int Int
myFunction = do
a <- get
let result = if a `mod` 2 == 0
then 10
else 20
modify (+1) -- Change the underlying state value
return result
{-
>> let x = 4
>> runState myFunction x
(10, 5)
>> x
4
-}
Again, this doesn’t actually “mutate” any data. When we pass x
into our State
function, x
itself doesn’t change! Instead, the function returns a completely new value. Now, different calls to get
can return us different values depending on the state. But this fact is encoded in the type system. We explicitly declare that there is an Int
value that can change.
Of course, there are times where we actually do want to change the specific values in memory. One example of this is if we want to perform an in-place sort. We’ll have the move the elements of the array to different spots in memory. Otherwise we will have to allocate at least O(n)
more space for the final list. In cases like this, we can use IO references. To sort an array, we’d want Data.Array.IO
. For many other cases, we’ll just want the IORef
type. Whenever you need to truly mutate data, you need to be in the IO
monad.
Looking at all these examples, what we see is that Haskell doesn’t actually limit us at all! We can get all the same mutability effects we have in other languages. The difference is that in Haskell the default behavior is immutability. We have to use the type system to specify when we want mutable data.
Contrast this with C++. We can get immutable data by using the const
keyword if we want. But the default is mutable data and we have to use the type system to make it immutable.
Conclusion
Immutability sounds crazy. But it does a huge amount to limit the kinds of bugs we can get. It seems like a big limitation on your code, but there are plenty of workarounds when you need them. The key fact is that mutable data is encoded within the type system. This forces you to be very conscious about when your data is mutable, and that will help you avoid bugs.
Want to see for yourself what the hype is about? Give Haskell a shot! Download our Getting Started Checklist and start learning Haskell!