Avoiding Mutations in JavaScript with Immutable Data Structures

Kyle Shevlin
InstructorKyle Shevlin

Share this video with your friends

Send Tweet
Published 3 years ago
Updated a year ago

This lesson teaches you the concepts of immutability, the difference between mutable and immutable data structures, and how to change data through mutable and immutable patterns. Immutable data is necessary in functional programming because mutations are a side effect. Our transformation of data should not affect the original data source, but instead should return a new data source with our updates applied.

To demonstrate the difference between mutability and immutability, imagine taking a drink from a glass of water. If our glass is mutable, when we take a drink, we retain the same glass and change the amount of water in that glass. However, if our glass is immutable, when we take a drink, we get back a brand new, identical glass containing the correctly drank amount. Perhaps a strange way to conceive of the action, but creating new data structures makes our methods pure and thread-safe, a benefit of functional programming.

Instructor: [00:00] Mutable data structures can be changed after creations, and immutable ones cannot. Mutations can be thought of as side effects in our applications. I'm going to demonstrate this through the use of an array.

[00:15] Let's create an array A. We'll equal it to a few values. We're going to assign a variable B to A. What this does is create a new variable with the same reference as A. We can see this by using a strictly equals equality check, because it will check the references are the same, not that the values are the same.

[00:37] Now, if I make an update to B by pushing a value onto it, if I log out A, we'll see that A has been updated as well. This is because A and B are not different arrays. They're a reference to the very same array.

[00:53] When we make changes to one, we actually make changes to the other. This can be problematic in our code. Functionality that's operating on B would change A, even if we didn't intend to do that. The same holds true for objects.

[01:07] If I create an object of A, we'll give a property foo, and we'll set it to bar. If I assign that reference to another variable B, and I make changes to B, such as reassigning foo to baz, if I log out A.foo, we'll see that it's been updated.

[01:27] This is problematic for functional programming, because it breaks the purity of our functions. When we make updates to data, we want to return brand new data structures that contain all the elements of the previous state of the data structure, plus our updates.

[01:43] For instance, push is a mutation on an array. As you saw, it changes all references to that same array. However, we could create an immutable push function. It'll receive a value and an array, and will return a brand new array by using the spread operator to create a clone, and then push the value onto that.

[02:08] If we create an array again of one, two, and three, this time, if I make a second array by pushing a value onto it using our new function -- we'll push four, and we'll use A -- we'll see that A hasn't been updated. We'll also see that A and B do not equal, because they are references to different arrays.

[02:32] We can create similar functionalities to handle changes to objects and so forth. One metaphor I like to use to describe this to people is the metaphor of taking a drink from a glass. I'm going to create two classes, a mutable glass and an immutable glass.

[02:50] We'll make a takeDrink method on both of them, and show the difference between handling it mutably and immutably. We'll start with the mutable glass. We'll have a constructor that takes a content and an amount, and assigns this.content to content and this.amount to amount.

[03:09] Then we'll create a takeDrink method that will take a value. What we will do is we'll update this.amount directly, and we'll return this instance of the glass. this.amount equals math.max. We're going to use this to guarantee that we never get less than zero amount in our instance, since we can't drink what's not there.

[03:32] Then we'll return this. If we create a glass, we'll call it MG1 equals new mutable glass. We'll give it a content of water and an amount of 100.

[03:45] Now, if we store the value returned to us after taking a drink, we can see that they are the same instance through a strictly equals comparison, and see that both MG1's amount and MG2's amount is the same, which makes sense, since they're the same instance.

[04:02] We'll log this out in our terminal, and we get true in each case. That's because we handle it mutably. We change the value directly, and we've changed the data structure after its creation. I'm going to comment these out, so that they don't turn up the next time I run the terminal.

[04:20] Now, we'll create an immutable glass. The constructor will look exactly the same, with a content and an amount. this.content equals content, this.amount equals amount. We'll make a takeDrink method that also takes a value, but here's where a big change occurs.

[04:37] When we take a drink, rather than returning back the same glass that I had before, I'm going to return a brand new glass with brand new content in the correct amount. To do this, we simply create a new immutable glass from within.

[04:53] We pass the content on again, because it should stay the same. We didn't turn water into wine or anything like that. Then for the amount, we'll do the same calculation, this.amount minus the value. The lowest it can be is zero, but because we return a whole new glass, we should see that when we create glasses, when we make updates, their instances are no longer the same.

[05:19] We'll create an IG1 variable. That's a new immutable glass. We'll also make it water, and we'll give it 100. Then if we store the glass returned to us after we take a drink from IG1 -- takeDrink, we'll do 20 -- what we should see is that when we compare IG1 and IG2, they aren't the same instance anymore.

[05:42] We can do that doing a strictly equals comparison. That should be false. We should see that the amount of IG1 hasn't changed, and thus does not equal the value in IG1. Both of these should be false.

Phillip
Phillip
~ 3 years ago

1:43 might be misunderstood when you say using spread gives you a clone. Yes, the effect of your code is you're making a clone, but one a should probably not think the spread operator means "clone". Rather "pull out all the contents". HTH

JP Lew
JP Lew
~ 3 years ago

Running Node 10 but I got a different result from you.

const push = val => array => [...array].push(val)
console.log(push(4)([1, 2, 3])) // 4

This returns the number 4, not the actual array.

const push = val => array => {
  const newArr = [...array]
  newArr.push(val)
  return newArr
}
console.log(push(4)([1, 2, 3])) // [1,2,3,4]

This is the only way I could return an array.

Correct me if I'm wrong, but the reason a === b is failing in the video is because we're comparing [1,2,3] === 4

Kyle Shevlin
Kyle Shevlininstructor
~ 3 years ago

Hey JP, good catch, I'll need to update this video. Push returns the number of elements in the array, so this is a bug on my part. Thanks for finding this.

JP Lew
JP Lew
~ 3 years ago

no prob. A bit pedantic on my part though, because your underlying point about immutability is still clear. Really enjoying the lessons so far, thanks!

Kyle Shevlin
Kyle Shevlininstructor
~ 3 years ago

Hey JP, video is fixed. 👍

Tony Catalfo
Tony Catalfo
~ 3 years ago

Could you give an example of how you could do this if you have an array of nested objects or a multi-dimensional array?

Kyle Shevlin
Kyle Shevlininstructor
~ 3 years ago

The concept is the same no matter the data structure. Whenever you make an update return a new data structure with the correct changes, leaving the original intact. I wrote this example in about a minute:

const updateObject = updater => obj => updater(obj)
const decrementValue = key => obj => ({
  ...obj,
  [key]: obj[key] - 1
})
const decrementTime = decrementValue('time')
const updateTimer = updateObject(decrementTime)
const timerFactory = time => ({
  time
})
const timers = [
  timerFactory(60),
  timerFactory(30),
  timerFactory(15)
]
const updatedTimers = timers.map(updateTimer)
console.log(updatedTimers) // [{ time: 59 }, { time: 29 }, { time: 14 }]

Every time we decrement the time on a timer, we return a new object by spreading the previous key/value pairs into a new object along with our change. Array.prototype.map() is an immutable method so we're getting a new array. So our updatedTimers is a brand new array with brand new objects and our originals are intact.

Tony Catalfo
Tony Catalfo
~ 3 years ago

are you overwriting push?

Kyle Shevlin
Kyle Shevlininstructor
~ 3 years ago

I'm not sure what you're asking. I never overwrite or monkey patch push in the lesson. I'm demonstrating how we can return a new array when we "push" an item onto our array. The function receives an array, makes a copy, pushes a value onto the copy and returns the copy.

A non-code metaphor might help perhaps. Imagine you wrote a paper for an class and you give it to your teacher to give you notes and edits. If the teacher makes their edits with mutations, they would hand you back the original paper with all of their edits on it, red marks here and there, and the original would be forever changed.

On the other hand, if the teacher made their edits immutably, they would make a fresh copy of your paper, and make their edits on the copy and give that to you. You could ask for the original back and nothing will have changed about it.

Tony Catalfo
Tony Catalfo
~ 3 years ago

clone.push(value) confused me. I understand it now. Thank you for the explanation.

Pavol
Pavol
~ 3 years ago

We're going to assign a variable B to A

You're assigning a value to a variable, not otherwise right?

Pavol
Pavol
~ 3 years ago

We can see this by using a strictly equals equality check, because it will check the references are the same, not that the values are the same.

This is not entirely true:

You should take special note of the == and === comparison rules if you're comparing two non-primitive values, like objects (including function and array). Because those values are actually held by reference, both == and === comparisons will simply check whether the references match, not anything about the underlying values. You-Dont-Know-JS/ch2.md at master · getify/You-Dont-Know-JS

Kyle Shevlin
Kyle Shevlininstructor
~ 3 years ago

Hi Pavol,

In the first case, I'm creating a variable b and assigning it to whatever the variable a is assigned to.

In the second case, you are correct that both == and === will tell us if the references are different. However, the point of the lesson is immutable data structures, not loose equality and coercion. My statement that a strict equality check will determine if two references are different is accurate. If I had added a note stating that one could use loose equality to achieve the same thing, it would have distracted from the lesson at hand.

Panks A
Panks A
~ 3 years ago

Hi Kyle,

Can you share how to interpret const push = val => array => { there are two arrow function used. Is this a nested function?

NHI TRAN
NHI TRAN
~ 2 years ago

is it better to make things mutable/immutable? Or does it depend on the use case? When it comes to making "pure" functions should all methods return immutable values? or was this video more on the concepts around mutable and immutable

Mark Waterous
Mark Waterous
~ 2 years ago

Can you share how to interpret const push = val => array => { there are two arrow function used. Is this a nested function?

I was unfamiliar with this syntax as well so I went down the rabbit hole. For anyone else who's being introduced to it for the first time, it's called a curried function and turns out there's a good video about it here on Egghead: https://egghead.io/lessons/javascript-currying-with-examples

Here's the initial stack overflow answer I found when searching: https://stackoverflow.com/questions/32782922/what-do-multiple-arrow-functions-mean-in-javascript

Kyle Shevlin
Kyle Shevlininstructor
~ 2 years ago

Hey Mark, I explain currying and curried functions in the very next lesson in the course. No need to go so far for an explanation.

It’s also pretty straight forward if you think through it for a moment. An arrow => implicitly returns any expression. This is why we need {} to create a function body and ({}) to implicitly return an object. Thus, we can see that this:

const add = x => y => x + y

Is the same as:

const add = function(x) { return function(y) { return x + y } }

Mark Waterous
Mark Waterous
~ 2 years ago

Thanks Kyle! Excellent explanation aside, what you're ultimately saying is maybe I should watch the entire course before I wander off trying to answer questions the course already covers? Reading that loud and clear :D

Kyle Shevlin
Kyle Shevlininstructor
~ 2 years ago

Ha, I’m saying I won’t leave you hanging in the course 😁 hope you enjoy the rest of it.

Serban Stancu
Serban Stancu
~ a year ago

I think it would be worth mentioning that spreading is not doing a deep clone. Using the spread operator on an array of objects won't give you the result you would expect.