Functional Programming in JS, part II - Immutability (Vanilla JS, Immutable.js and Immer)
mpodlasin
Posted on October 6, 2020
This is a sequel to my article Functional Programming in JS, part I - Composition (Currying, Lodash and Ramda) . In that previous article I was focusing on what I believe to be the most powerful concept in functional programming - composability.
But a concept in functional programming that is the best established in modern mainstream development is probably immutability. On front-end it was was popularized by projects like React and Redux, where immutability of state is important for the library to work properly.
Because immutability is already quite popular in modern programming, this article will be split into two parts.
In the first part I will give a quick introduction to the concept of immutability, giving simple examples in JavaScript and providing some practical motivations for favoring immutable data structures. This part is good for those who still don't really grasp what is the difference between mutable and immutable data structures or those who are not sure where JavaScript stands when it comes to immutability.
In the second part I will focus on how immutability can be achieved in JavaScript. We will see how to write immutable code with vanilla JavaScript as well as libraries (Immutable.js and Immer). At the end I will give my opinions on which solution will be the best for you and for your team.
Let's get started!
Introduction to Immutability
Immutability is actually a very simple concept, once you get to know it.
Let's see it on an example in JavaScript. Say we have a string assigned to a variable:
const someVariable = 'some string';
We want to get only the first three letters of that string. There is a method called slice
, that allows you to do just that:
console.log(
someVariable.slice(0, 3)
);
With such parameters, slice
will return a substring that starts at index 0 and ends at index 3 (not including that last index). So as a result we should get first 3 letters of our string.
After running that code we see som
printed to the console. Perfect!
But let's now check what happens if we modify our code a little. Let's see what value someVariable
has, after we have used the slice
method:
console.log(
someVariable.slice(0, 3)
);
console.log(someVariable);
First the som
get's printed and right after we see some string
printed.
This might seem obvious, but notice the curiosity here. In object oriented programming methods (like slice
) are usually used to modify the object on which we are calling a method. And yet here our string does not get affected in any way by running slice
method on it.
That's because in JavaScript all strings are immutable. You cannot change them with any methods. You can only run methods on them that return new strings (like our som
string, which we have printed).
In fact, in JS you cannot even modify a string like so:
someVariable[0] = 'x';
console.log(someVariable);
After running that code, some string
will appear in the console, with no x
in sight.
This outcome is certainly surprising for people who have some experience in other programming languages, like C/C++ for example.
Let's now do a similar experiment, but this time with arrays. It turns out that arrays also have a slice
method, that behaves basically in the same way, but instead of selecting characters, we are selecting elements from an array:
const someVariable = [1, 2, 3, 4, 5];
console.log(
someVariable.slice(0, 3)
);
console.log(someVariable);
After running that code, we see [1, 2, 3]
printed first and then [1, 2, 3, 4, 5]
. So it works the same as in the case of strings - slice
does not change the original array in any way.
Arrays however have a second method, similar to slice
in both name and what it does, called splice
(note the p
!).
Let's change slice
to splice
then and see what happens:
const someVariable = [1, 2, 3, 4, 5];
console.log(
someVariable.splice(0, 3)
);
console.log(someVariable);
First [1, 2, 3]
gets printed, just as before, but then we see... [4, 5]
being printed. That's different!
As you can see, splice
"cut out" first three elements, returned them as a result and left our original array with only two elements left.
Our original array has been modified. In fact, even if someone would save that array to some other variable, it still would not help:
const copyOfSomeVariable = someVariable;
someVariable.splice(0, 3);
console.log(copyOfSomeVariable);
After running that code, we get [4, 5]
as a result again.
Note that we ran splice
method on someVariable
and yet the side effect - modifying an array - is visible also in copyOfSomeVariable
.
That's because we have two variables, but they point to the precisely the same object in computer's memory. It's literally the same array, just referenced by two (and potentially more) variables.
If you have already worked on some commercial projects, involving many programmers or even multiple teams of programmers, you might start thinking: "Wait, isn't that kind of dangerous to just modify a data structure globally, for everyone? What if those first three elements where needed by someone in a completely different place in our codebase?".
And this fear would be 100% justified. This is one of the main motivations for keeping your data structures immutable. After all, can you be really sure that you are allowed to modify this object? What if there was data inside that object which someone else needed? Seems like a very fragile way to write code, doesn't it?
That's why I personally love immutability and why it's my default way to code in JavaScript. Instead of risking running into some bugs in a massive codebase, I can just write immutable code. This will ensure that every changes to objects I make are only accessible to me and are fully controlled by me.
Of course there are moments when you actually don't want immutability. It definitely comes at a cost of higher complexity (both in speed and memory efficiency). So if you are working with extremely large data structures you need to be careful.
But most programmers are working on a day to day basis with relatively small objects. In most cases writing immutable code is a good rule of thumb. It's like choosing const
over let
in JavaScript. Use const
all the time and only default to let
if you know you need it. The same works for immutability.
How to write immutable code in JavaScript
Vanilla JS
As we showed in our motivating example, JavaScript is kind of awkward when it comes to immutability. Some of it's values are immutable (like numbers, strings and booleans) and others are mutable (arrays and objects and some ES6 additions like Maps, Sets etc...).
On top of that, some methods on mutable values work in immutable way (like slice
), while other methods mutate their values (like splice
).
This makes writing immutable code in JavaScript a bit tricky for the inexperienced. I have personally seen many times people who thought they were writing immutable code in JS, but in fact they didn't.
It doesn't help that mutating objects is often something that becomes apparent only in corner cases. You have seen that splice
appears to work in the same way as slice
- in our examples both times it returned a [1, 2, 3]
array. If we hadn't checked what happened to original array, we might have think that they work exactly the same. Scary!
On the other hand, a lot of syntax introductions that began in ES6 are pushing the language in the right direction. Especially the spread operator allows you to write immutable code in an easier way. With a bit of help of destructuring, writing such code in JavaScript becomes quite pleasant and readable.
Let's see how you could update property of an object, using destructuring.
Usually people update object's property like so:
const someObject = {
a: 1,
b: 2,
};
someObject.a = 100;
I hope that it is clear by now that this code mutates original object. Even if it was stored in some other variables.
Spread operator allows us to change this code to:
const newObject = {
...someObject, // we are using spread operator here
a: 100,
};
We now have two objects - someObject
and newObject
. You can check that someObject
was not affected in any way. Indeed, we can run:
console.log(someObject);
console.log(newObject);
This prints {a: 1, b: 2}
first and {a: 100, b: 2}
second.
There are some caveats here. You can see that the code is a bit verbose. What previously took us one line, here takes up 3 lines of code.
But more importantly, it's easy to make some mistakes here. For example reversing the order in which newObject
properties are constructed will result in a bug:
const newObject = {
a: 100, // this line is now first, not second
...someObject,
};
console.log(someObject);
console.log(newObject);
Running this code will print {a: 1, b: 2}
first, which we expected, and {a: 1, b: 2}
second, which is wrong! Our object was not updated, like we intended!
That's because spread operator basically iterates over properties of someObject
, applying them to our newObject
. In the end it sees a new property a
set to 100, so it updates that property.
In the second example the reverse happens - first a
gets set to 100 and just then we iterate over someObject
. Because a
is set to 1 in someObject
, a property with that value gets created on our new object, overwriting a: 100
entry.
So you can see that - although possible in JavaScript - writing immutable code requires a lot of knowledge and awareness from the programmer. If you and your team know JavaScript well, this won't be a problem. But if many of developers in your team write in JavaScript only occasionally and know it just superficially, you might expect some bugs to occur.
This awkwardness of writing immutable code in JavaScript is probably why at some point there appeared a lot of "immutable-first" libraries in JavaScript. Probably the most popular of them is Immutable.js.
Immutable.js
Immutable.js is basically a set of data structures that are supposed to replace mutable vanilla JS data structures.
But instead of providing replacements only for array and object (by - respectively - List and Map in Immutable.js), it also gives a much longer list of interesting data structures, like OrederedMap, Set, OrderedSet, Stack and much, much more.
Those custom data structures have a great amount of methods that make working with them quite easy and pleasant. And yes, absolutely all of those methods work in an immutable way, by returning a new data structure and leaving the previous one unchanged (unless they are specifically and explicitly designed to allow for mutations, for example in cases where it would be more efficient).
Immutable.js data structures are also written to be as efficient as possible, with time complexities even stated in the documentation next to each data structure!
But of course there are also some problems. The biggest one for me was constant need to jump between native JS values and Immutable.js values. Libraries usually expect and return JavaScript objects and arrays, which you need to converse back and forth between Immutable.js data structures. This is cumbersome and difficult to keep track of.
When I used Immutable.js, there were points when I was getting an array from a library A, had to convert it to Immutable.js List only to make some small changes and then convert it back to a JavaScript array, to pass it to a library B. Pretty pointless, so when we stopped doing that, we used Immutable.js less and less in the project, until there was really no point anymore.
On top of that, when I was using Immutable.js with TypeScript I was running into weird problems with typing. Perhaps this is fixed by now (I haven't used Immutable.js recently), but this was the last straw that made me stop using the library.
Still, depending on specifics of your projects, using Immutable.js might turn out to be a real pleasure. Simply try it out for yourself!
Immer
Immer is a completely different twist on "writing immutable code" idea.
Instead of changing our behaviors to write immutable code, Immer attempts to change mutable code... to be immutable.
It does it by wrapping regular - even mutable - JavaScript in a special function, which tracks what changes we want to make, but then performs them in immutable way, by creating a new value, instead of changing the original one:
import produce from "immer"
const someObject = {};
const result = product(someObject, draftObject => {
draftObject['some key'] = 'some value';
});
console.log(someObject);
console.log(result);
After running this code someObject
variable will print {}
, while result
will print - as expected - {'some key': 'some value'}
.
So even though we wrote a code that would mutate the object in a regular JavaScript:
draftObject['some key'] = 'some value';
Immer makes sure we don't actually do any mutations, but create a new object with changes specified in function passed to produce
.
This approach definitely has some pros, the most important being that it allows you to stop thinking if your JavaScript is truly immutable. You can write whatever you want and Immer's mechanism will guarantee immutability for you. This reduces errors and allows even beginners to write immutable code in a way that is probably more familiar to them than some exotic functional patterns.
The obvious con is of course necessity of wrapping everything in a produce
callback function.
But, in my opinion, the biggest drawback of Immer is it's lack of composability.
The thing is, the code wrapped in produce
is still an imperative, mutable code. If you end up with a massive, complicated produce
callback and you want to refactor it to two or more smaller functions, you can't really do it easily. You need to define multiple new produce
callbacks and finally glue them together.
The end result often is ending up with very small produce
functions, like:
function updateFirstElement(array, element) {
return product(array, draftArray => {
draftArray[0] = element;
});
}
That's because such a function is more reusable in different scenarios. That's what functional programming favors - small functions, which are easily reused.
But with a code like that, you might as well just revert back to using basic ES6 features:
function updateFirstElement(array, element) {
const [_, ...rest] = array;
return [element, ..rest];
}
This way you end up with a code that is not much worse, wihout a need to use an external library.
However Immer does have another interesting solution for writing immutable functions. It allows you to call produce
functions in curried form. So our example changes to:
const updateFirstElement = produce((draftArray, element) => {
draftArray[0] = element;
});
Now this definitely does look very elegant. If you swear by mutable code being more readable, than Immer will probably work very well for you.
But for people who already got accustomed to functional way of coding in JavaScript (or want to learn it), it might still not be worth it to load an external dependency just to turn a 4 line function into a 3 line function.
How useful will Immer be, will - again - depend on the use cases and specifics of your codebase.
Conclusion - so what do I actually use?
So which one should you use to write immutable code? Native JavaScript? Immutable.js? Immer?
I would advise to learn JavaScript functional patterns (like destructuring and spread operator) anyways. They are becoming very popular in modern codebases, whether you yourself like them or not.
When starting a commercial project, I would start simple, by working only with native JavaScript.
If you notice that you or your teammates have trouble writing immutable code without bugs or it becomes tedious and unreadable, then I would recommend looking at the libraries.
Immutable.js will work especially well if you need some more advanced data structures or if data structures in your application are unusually complex. In that case, the number of data structures and methods available in Immutable.js to manipulate those structures will be a massive help.
On the other hand, if your team feels much more comfortable writing imperative, mutable code (or simply prefers it), then you should of course try Immer.
That's it!
I hope this article gave you a deeper understanding of immutability and gave you an overview and how you can start writing immutable code in JavaScript.
If you enjoyed this article, follow me on Twitter, where I am regularly (immutably!) posting articles on JavaScript and functional programming.
Thanks for reading!
Posted on October 6, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 6, 2020