What is it?
Immutability is a constraint on our data types that says “this data type cannot be modified”. On the surface this sounds like very counter intuitive thing to want. How on earth can we do anything if we can’t change any data?!
Well, we do it by making a copy. Here are two examples in javascript the first uses a mutable data structure, the second is how we would have to do it if the data type was immutable in javascript
let mySandwich = { bread: "brown", butter: false}// This is a mutable update
function mutableButterSandwich(sandwich) {
return sandwich["butter"] = true
}// this is an immutable update
function immutableButterSandwich(sandwich) {
return {...sandwich, butter: true}
}
In the first example we mutate mySandwich
meaning if we call the mutable butterSandwich
then console.log(mySandwich)
we will see that it has changed:
mySandwich = { bread: "brown", butter: false}
mutableButterSandwich(mySandwich)
console.log(mySandwich)
// You should see: { bread: "brown", butter: true}
In the second example we use the spread operator to take everything that was inside of the sandwich we get passed, and put it into a new object. We also add a butter
key set to true
. This will override the existing butter key and set it to true
. Because we return a copy, we have not changed the original mySandwich
variable:
mySandwich = { bread: "brown", butter: false}
immutableButterSandwich(mySandwich)
console.log(mySandwich)
// You should see: { bread: "brown", butter: false}
What does this mean for our programmes?
The biggest implication of immutable data is exactly what you’ve just seen. When data types are immutable the return value of our functions becomes very important, because you can’t modify anything outside of a function. So in order to do anything you have to return something from a function and use that.
Immutable data types force our functions to become something called referentially transparent. A function is referentially transparent if for a given input, you can swap the function out for its return value and have nothing else in the state of the whole program or world be any different. This is a referentially transparent function:
function add(x, y) { return x + y }
This is not:
function mutableButterSandwich(sandwich) {
return sandwich["butter"] = true
}
It’s important to note referential transparency doesn’t mean “for every input into a function we always return the same output”. It means very specifically for a given input, you can swap the function out for its return value and have nothing else in the state of the whole program or world be any different. For example this function is not referentially transparent:
const thing = { number: 1}function doAThing(y) {
thing["number"] ++
return y
}
A good litmus test is “if I ran this function twice in a row with the same input, would everything in the program / wider world be the same?”. If you log something during a function, then the answer would be no.
If we have immutable data structures, we are forced to have more functions that are referentially transparent, because in order to do anything in the program we have to return the thing we want to do something with. Let’s add a filling to our sandwich using mutable data structures:
let mySandwich = { bread: "brown", butter: false}function mutableButterSandwich(sandwich) {
return sandwich["butter"] = true
}function addFilling(sandwich, filling) {
sandwich["filling"] = filling
return sandwich
}// Because we are using mutable data we can just call one function
// then the other, and not even worry about what is returned.mutableButterSandwich(mySandwich)
addFilling(mySandwich, ["cheese"])console.log(mySandwich)
Now let’s try it with immutable data:
let mySandwich = { bread: "brown", butter: false}function immutableButterSandwich(sandwich) {
return {...sandwich, butter: true}
}function addFilling(sandwich, filling) {
return { ...sandwich, filling: filling }
}// If we want to have a buttered sandwich with filling, we need
// to take the result of the butter sandwich function and feed
// it into the addFilling function.butteredSandwich = immutableButterSandwich(mySandwich)
addFilling(butteredSandwich, "cheese")
You can see in the immutable version we need to take the result of the previous function and feed that into the next one. Often functional languages have ways to make this easy, for example Elixir has a pipe operator so you can do this:
butteredSandwich(mySandwich) |> addFilling("cheese")
This is also a good grounding to start thinking about composition of functions, which we can talk about in the next post.
Is it good?
The more referentially transparent functions you have, the less chance you have of being able to rely on something that can change from under you. Let’s look at a classic bug mutable state allows:
let mySandwich = { bread: "brown", butter: false}
let fillings = ["cheese", "crisps", "quorn"]function listFillings(fillings) {
while (fillings.length > 0) {
console.log(fillings.shift());
}
}listFillings(fillings)
addFilling(mySandwich, fillings[1])
Because listFillings
mutates, after it runs fillings
is an empty array, meaning if we then try to use a filling it’s no longer there! This kind of bug is not even possible in an immutable world, as listFillings
would have to change.
let mySandwich = { bread: "brown", butter: false}
let fillings = ["cheese", "crisps", "quorn"]function listFillings(fillings) {
fillings.map(function(filling) { console.log(filling) })
}listFillings(fillings)
addFilling(mySandwich, fillings[1])
Here, fillings
hasn’t changed after listing them. Now if the first version set off alarm bells in your head that’s great, and you might think “I would never write it that way anyway”, but with an immutable language it’s not even possible to. This is good.
Immutability is the idea of not changing data in place. We instead make copies and pass those copies around. It forces your functions to be referentially transparent, which eliminates a whole class of bugs.
Practice taking functions or methods in programmes you know and making them referentially transparent by forcing yourself to not mutate any data types. Then try linking together a few of those immutable functions and compare them to what they were originally.