Immutable Data Structures, Concepts and Examples

Published

The growing popularity of Immutable Data Structure libraries has brought about a clear distinction in how JavaScript variables can be modified. Variables can either be modified directly, or they can be treated as Immutable Data Structures, and require a new variable or Object each time a change is made.

At first I was skeptical about what benefits enforcing immutability provided (i.e. was it more than just a trend). There were a few pretty cool benefits in addition from those listed on the Immutable JS GitHub page, such as Lazy Evaluation and Persistent Data Structures (Control-Z to revert app state anyone?). What really sold me was how Immutable data structures could be used within React/Flux applications to cut down on excess renders. While renders may be cheap when dealing with proper React Components, the re-renders of non-React libraries, like map vector data drawn with OpenLayers 3, can become quite costly.

Direct Mutation

Variables can be changed in place, or mutated directly. After changing a variable in this way, no re-assignment is necessary, and all references to the original variable / Object remain in tack. Examples function are Array.prototype.push() and Array.prototype.pop(), since each function will modify the underlying array directly.

//create basic object representing a person
var person = {
  name: 'Taylor',
 age: 28
};

//assume the person is hired as a developer; when definiring the 
//  developer it actually references the same object as person
var developer = person;

//our person's birthday occurs and they turn 29!
//  - we will mutate the person object directly here
person.age = 29;

//notice when we ask the developer how old they are, they are 29
//  - since we mutated the person object directly, and the developer 
//   references the person object, the change will be reflected on
//   the developer object, and anywhere else in our code that person
//   is referenced
console.log( developer.age ); 

Here is another example with Array.prototype.push() function:

//create an original array of friends
var friends = ['jim','tommy','jacob'];

//reference our array of friends in super-set object of people we know
var knownPeople = {
 friends: friends,
 associates: ['samantha','tim']
};

//mutate the friends array directly by adding lucy
friends.push( 'lucy' );

//notice that when asking for the number of friends through the reference 
// on the known people object, the change made above is reflected; this 
// is because the friends array was mutated directly, and the reference 
// to it from the known people object is still in tact
console.log( knownPeople.friends.length ); //4

Immutable Data Structures

Instead of modifying a variable directly in-place, a new variable can be created based on the original, and have the desired changes made to the new variable. By definition, immutable data structures cannot be changed, so with this approach, they are not changed directly, but instead “re-assigned” to a new value.

Example of JavaScript functions that modify Arrays and Objects in an immutable fashion are Object.prototype.assign() with a new target Object, Array.prototype.filter(), or Array.prototype.concat().

//create arrays of bike manufacturers
var mountainBikes = ['Santa Cruz','Trek','Salsa'];
var bmxBikes = ['Mongoose','Haro'];

//concatenate arrays into single allBikes array
var allBikes = mountainBikes.concat( bmxBikes );

//notice the mountainBikes.concat() function did not mutate the mountainBikes
// array directly, but instead returned a new array that comprised of both
// mountainBikes and bmxBikes
console.log( mountainBikes.length ); //3
console.log( allBikes.length ); //5

Below is an example of modifying an Object in an immutable fashion using the Object.prototype.assign() function. Notice the target of the assign() function is a new Object (i.e. the open and close {} braces passed as the first parameter). Object.prototype.assign() is making a shallow copy of all properties on the person object and applying them to the new Object target. This is essentially what Immutable libraries are doing under the hood when when a modification to an immutable Object is requested (however they are also highly optimized to perform these operations efficiently).

//create a new person object
var person = {
 name: 'taylor'
};

//use Object.assign() with a new Object as the target (first paremeter) to
//  essentially copy the original person object and apply the retired: true
// property; using Object.assign() will ensure that the original person 
// object is not mutated directly, and will instead return a new object
//  with the assignments made, which is how immutable libraries work
var retiree = Object.assign( {} , person , { retired: true } );

console.log( retiree.retired ); //true since the new object was saved into retiree
console.log( person.retired ); //undefined; Object.assign() didn't modify original obj

Various libraries have been created to provide data structures with easy APIs for performing these “create a whole new Object just to modify one property” type operations. The example covered in this article is Immutable JS, where an Immutable JS Map (essentially a key->value HashMap) can be modified simply by executing map.set(“name”,”taylor).