Immutability is the basic principle of functional programming, which also has much to offer object-oriented programs. In this article, I will tell you about what exactly is the cornerstone of immutability, how to use this concept in JavaScript, and why it is useful.
What is immutability?
The book definition of changeability reads as follows: "the tendency of an object to change or transform." In programming, we use this word when we mean objects whose state can be changed over time. The immutable value is exactly the opposite; it will never change after creation.
If this seems odd, let me remind you that most of the values ​​that we use all the time are actually immutable:
var statement = "I am an immutable value"; var otherStr = statement.slice(8, 17);
I think that no one will be surprised if he finds out that the second line in no way changes the string value of the
statement . In reality, there are no string methods that would modify the string on which they perform the action, they all return new strings. The reason is that strings are immutable - they cannot be converted; we can only create new strings.
Strings are not the only immutable values ​​embedded in JavaScript. Numbers are also immutable. In general, can you imagine an environment where the calculation of the expression
2 + 3 changes the value of the number
2 ? It sounds absurd, although we do it all the time with our objects and arrays.
')
In JavaScript, variability is abundant
In JavaScript, strings and numbers are designed to be immutable. However, consider the following example using arrays:
var arr = []; var v2 = arr.push(2);
What is the value of
v2 ? If the arrays behaved according to strings and numbers, then v2 would contain a new array with one element inside - 2. However, this is a different case. Instead, the link
arr was changed to contain a number, and
v2 contains a new length
arr .
Imagine the
ImmutableArray type. His behavior, borrowing from numbers and strings, would look like this:
var arr = new ImmutableArray([1, 2, 3, 4]); var v2 = arr.push(5); arr.toArray();
Similarly, an immutable associative array, which could be used instead of most objects, would have methods for “setting” properties that would not actually set anything, but would return a new object with the required changes:
var person = new ImmutableMap({name: "Chris", age: 32}); var olderPerson = person.set("age", 33); person.toObject();
Just as
2 + 3 does not change the values ​​of either 2 or 3, the celebration by someone of their 33rd birthday does not cancel the truth that a person was 32 years old.
JavaScript immutability in practice
JavaScript (for now) does not have immutable lists and associative arrays, so now we need a third-party library. There are 2 very good libraries available. The first one is
Mori , which allows you to use persistent data structures from ClojureScript, as well as JavaScript support APIs. The second is
immutable.js , written by developers from Facebook. In this demonstration, I will use immutable.js for the simple reason that its API is more familiar to JavaScript developers.
In this demonstration, we will consider the principle of working with immutable data in Minesweeper. The board is represented by an immutable associative array, in which
tiles are the most interesting part of the data. This is an immutable list of immutable associative arrays, where each of the latter (i.e. assoc. Masses -
approx. Lane ) represents a separate tile on the board. The entire construct is initialized using JavaScript objects and arrays, and then becomes “immortal” thanks to the
fromJS function from immutable.js:
function createGame(options) { return Immutable.fromJS({ cols: options.cols, rows: options.rows, tiles: initTiles(options.rows, options.cols, options.mines) }); }
The rest of the core of the game logic is implemented as functions that take this immutable structure as their first argument and return a new instance. The most important function is
revealTile . When called, she marks the tile as open to open it. With a variable data structure, it will be very simple:
function revealTile(game, tile) { game.tiles[tile].isRevealed = true; }
But with immutable structures like those suggested above, it becomes more than difficult:
function revealTile(game, tile) { var updatedTile = game.get('tiles').get(tile).set('isRevealed', true); var updatedTiles = game.get('tiles').set(tile, updatedTile); return game.set('tiles', updatedTiles); }
Fe! Fortunately, such things are not uncommon. Therefore, in our toolkit there is a method for such purposes:
function revealTile(game, tile) { return game.setIn(['tiles', tile, 'isRevealed'], true); }
The
revealTile function
now returns a
new immutable instance in which one of the tiles is different from the previous version.
setIn is null-stable and is filled with empty objects if any of the parts of the key do not exist. In the case of the Minesweeper board, this is not desirable, since the missing tile means that we are trying to open the tile outside the board. This can be mitigated by using
getIn to search for a tile before performing actions on it:
function revealTile(game, tile) { return game.getIn(['tiles', tile]) ? game.setIn(['tiles', tile, 'isRevealed'], true) : game; }
If the tile does not exist, then we simply return the existing game. It was a brief acquaintance with immutability in practice, if you want to understand more carefully, go to this
codepen , it contains the full implementation of the rules of the game Minesweeper.
And what about performance?
You might think that this will have a significant performance degradation and in some ways you will be right. Every time you add something to an immutable object, we need to create a new instance by copying the existing values ​​and adding a new value to it. This will definitely lead to more memory load, as well as to greater computational costs than would be required for the mutation of an individual object.
Since immutable objects never change, they can be implemented using a strategy called “structural sharing”, which generates much less memory costs than you would expect. Compared to embedded arrays and objects, costs will still exist, but they will have a fixed value and can usually be compensated for by other benefits that are available due to immutability. In practice, in many cases the use of immutable data will increase the overall performance of your application, even if certain operations become more costly in isolation.
Improved change tracking
In any UI framework, one of the most difficult tasks is to search for mutations. This is such a well-known test that EcmaScript 7 provides a separate API to help track object mutations with better performance:
Object.observe () . While some people like this API, others feel that this is not the answer to that question. In any case, it does not solve the problem of tracking mutations properly:
var tiles = [{id: 0, isRevealed: false}, {id: 1, isRevealed: true}]; Object.observe(tiles, function () { }); tiles[0].id = 2;
The mutation of the
tiles [0] object does not activate our mutation observer, therefore, the proposed mutation tracking mechanism is not suitable even for a trivial application case. How can immutability help in this situation? Suppose that an application has state
a , and a potentially new application has state
b :
if (a === b) {
If the state of the application has not changed, then this is the same instance as before and we do not need to do anything at all. This definitely requires that we track the state-containing link, but the whole problem now comes down to the fact that we need to manage a single link.
findings
I hope that in this article you have gained some knowledge of how immutability will help you improve your code, and that a continued example can shed light on the practical aspects of working in this direction. Immutability is gaining popularity and this will not be the last article on this topic that you will read this year. Try it, and I promise that you will like it very quickly as much as you liked me.