DEV Community

loading...
Cover image for You don't need mutation

You don't need mutation

LUKE知る
Developer and gamer | Host of hablemos.dev | Owner of Vangware
Updated on ・5 min read

The original title was "You don't need variables", but I'm not saying that you don't need a way of storing data in memory, because you do ... I'm just saying that once you save something, you should never change that value. So yes, this article is about immutability and how great it is.

Why is mutation evil™?

Mutation is at the core of the vast majority of bugs I had to deal with in my career, and I'm willing to bet it's at the core of yours too. Mutation basically means changing the value of something, which seems to be harmless, until you're working on a team and you change something that shouldn't be changed. This kind of accidents happen all the time in JavaScript and languages like it, because when you call a function and pass an object to that function, you're actually passing a reference to it, instead of a copy. Let's see a simple example:

/**
 * We have an user object with 2 properties,
 * name and age.
 */
const user = {
    name: "Luke",
    age: 31
};

/**
 * We have a function that gives us the user with
 * the age change to the next year value (+1)
 */
const userNextYear = user => {
    user.age += 1;
    return user;
};

const nextYear = userNextYear(user);

// Luke's will be 32
console.log(
    `${nextYear.name}'s will be ${nextYear.age}`
);

// Luke's age is 32
// oh no!
console.log(`${user.name}'s age is ${user.age}`);
Enter fullscreen mode Exit fullscreen mode

Now, this is obvious because all the code is in the same place, now imagine the surprise if you are importing that function from somewhere else. Basically this happens:

import { someUtil } from "somewhere";

const object = { foo: "bar" };

someUtil(object);

// `object` went into The Twilight Zone!
// Its value is unpredictable @_@
Enter fullscreen mode Exit fullscreen mode

How can we resolve this?

There are several approaches to resolve the issues presented by mutation, some better than others. The worst one (and one of the most common solutions) is to just make a copy of the object before passing it to a function:

import { someDeepCopyUtil } from "someLibrary"
import { someUtil } from "somewhere";

const object = { foo: "bar" };
const copy = someDeepCopyUtil(object)

someUtil(copy);

// object is unaffected, yey!
Enter fullscreen mode Exit fullscreen mode

The problem with this approach is that you're doing extra work everywhere instead of just avoiding mutations altogether. The other solution is to write your functions without doing mutations, just returning copies with changes on them. This type of functions are called pure functions, and avoiding mutations is what we call immutability. Going back to the first example:

const userNextYear = user => ({
    ...user,
    age: user.age + 1
});

// This returns a copy of user:
userNextYear(user);

// So this still has the the original value:
user.age;
Enter fullscreen mode Exit fullscreen mode

This is great for small functions, that do little changes to small objects, but the problem is that this becomes super complex if the object has nested values:

const object = {
    foo: {
        bar: [0, 1, 2, 3],
        other: {
            value: "string"
        }
    }
};

const updateOtherValue = value => object => ({
    ...object,
    foo: {
        ...object.foo,
        other: {
            ...object.foo.other,
            value
        }
    }
});
Enter fullscreen mode Exit fullscreen mode

Which is obviously way more complex than just doing a mutation:

const updateOtherValue = value => object => {
    object.foo.other.value = value;
    return object;
}
Enter fullscreen mode Exit fullscreen mode

Luckily for us, there is a great library that allows us to write code as we were doing mutations, but actually produces an immutable copy of the object, and it's called immer. This library allows us to write our updateOtherValue function like this:

import { produce } from "immer";

const updateOtherValue = value => object =>
    produce(object, draft => {
        draft.foo.other.value = value;
    });
Enter fullscreen mode Exit fullscreen mode

We end up with the best of both worlds: Code as simple as with mutations, but actually immutable. Now let's go back to JavaScript without libraries for a second...

Things to avoid from vanilla

JavaScript itself provides some methods that actually aren't pure, so they mutate the original object. For example Array has a few methods in its prototype like push or pop that actually change the original value. So you end up with similar issues to the first example:

const array = ["foo", "bar"];
const addValue = value => array =>
    array.push(value);

const addFooBar = addValue("foobar");

// This changes the original array:
addFooBar(array); // ["foo", "bar", "foobar"]
Enter fullscreen mode Exit fullscreen mode

You can either just avoid not pure methods and functions, like this:

const array = ["foo", "bar"];
const addValue = value => array =>
    array.concat(value);

const addFooBar = addValue("foobar");

// This returns a copy of the array
addFooBar(array); // ["foo", "bar", "foobar"]
// But the original is untouched :D
Enter fullscreen mode Exit fullscreen mode

Or, going back to immer, we can just to this:

import { produce } from "immer";

const array = ["foo", "bar"];
const addValue = value => array =>
    produce(array, draft => draft.push(value));

const addFooBar = addValue("foobar");

// Same effect as the pure approach 🎉
addValue(array);
Enter fullscreen mode Exit fullscreen mode

There are several sites that cover the mutation functions, one of them that I recommend for array is this one: doesitmutate.xyz. It lists all the array methods and has a flag for the ones that produce mutations (so those are the ones you need to avoid).

One thing worth mentioning is that the DOM APIs are full of mutations, so if you want to change something dynamically on a WebApp you need to do mutations. Luckily for us, libraries like React, Preact, Vue and others have an abstraction layer over the DOM called VDOM, that make the DOM behave in a "pure" way by letting us update it's state without having to do the mutations ourselves, and in a consistent and safe way.

Classes and mutation

So this article is in the same series as You don't need classes, and is pretty close to it. Classes generally encourage saving values inside the class, and changing those, so this is yet another reason to avoid classes and just use pure functions and values instead. Even if you decide to still use classes, try to avoid mutations, by returning new instances of the classes with the new values in them.

What about performance?

JavaScript and languages like it have a great garbage collector that takes care of the values you're not using any longer. The vast majority of the cases, as soon as you create a copy of something and you don't use that something any longer, the original gets removed from memory.

Still, the cost in performance is way to low compared to the benefits that you get from never doing mutations.

Do you actually need mutations?

Similar to the previous post in this series, I finish with an open question for the readers of the post to really think about this: Do you really need to mutate that value? Don't you have a way of resolving that issue without doing a mutation? I'm not saying this will always be the solution, but it should be the default.

Thanks for reading this and if you don't agree with something said in here, just leave a comment and we can discuss it further.

See you in the next post of this series!


Edit:

Updated the title because some folks considered the previous one to be a "click-bait". Previous title was: "You don't need variables", because the values you set shouldn't vary/change, and you should use const instead of let/var. But I prefer to update the title so folks don't complain.

Discussion (22)

Collapse
codenameone profile image
Shai Almog

While I agree to some degree with the points made I'd like to stress that wide sweeping generalizations are a bane of our industry. One of the reasons most of the bugs we see are in mutable code is because the vast amounts of mutable code. There are plenty of immutable bugs such as the common:

myString.substring(3);
Enter fullscreen mode Exit fullscreen mode

Which should be:

String newString = myString.substring(3);
Enter fullscreen mode Exit fullscreen mode

Did you actually measure the performance of immutable vs. mutable code?

In Java primitives are up to 20x more performant than object wrappers and the JVMs GC is far more efficient than JavaScript. This might not apply to the same level in JS which is more dynamic.

Collapse
lukeshiru profile image
LUKE知る Author

The main difference is that if you're working with immutability and pure functions, then this:

myString.substring(3);
Enter fullscreen mode Exit fullscreen mode

Shouldn't happen. This is because every function returns a value that needs to be stored in a new constant, or returned or whatever, so you pretty much never just run a function or method without doing something with the output. In JS we have tools like ESLint with linting rules that will allow us to detect this quite easily.
The bugs related to mutations generally are unexpected, because you called an impure function, or a mutation method, or just assigned a value where you shouldn't.
About performance, it depends on what are you doing and how. Immer for example is actually really smart, using a Proxy to track the changes and then using references to the original object with everything that didn't changed.
Still, at least from my PoV, the performance cost of immutability is nothing compared to the value they have.

Collapse
danbamikiya profile image
Dan Bamikiya • Edited

Hey @lukeshiru how would you handle this without mutation?

/**
 *
 * This is like a Promise.then() where there
 * is an array of callbacks and each callback is called on the value
 * of the previous callback.
 * The value of the final callback value is returned.
 *
 */

async function invokeCallbacks(response, callbacks) {
    let value = response;

    for (const callback of callbacks) {
        if (!(typeof callback === 'function')) return;
        value = await callback(value);
    }

    return value;
}
Enter fullscreen mode Exit fullscreen mode
Collapse
lukeshiru profile image
LUKE知る Author • Edited

You have to remember that async/await is still just Promise internally, so you can use all the tools that Promises have without the need for async/await. If the array of callbacks is actually just functions that don't return promises, then you can just use a reduce like this:

const invokeCallbacks = (callbacks = []) => initialValue =>
    callbacks.reduce(
        (value, callback) => callback(value),
        initialValue
    );

// and then you use it like this:
const invoke = invokeCallbacks([/* Array of callbacks */]);
const output = invoke(/* Initial value */);
// Do whatever you want with the output
Enter fullscreen mode Exit fullscreen mode

If those callbacks are async (return promises), then you could write it like this:

const invokeCallbacks = (callbacks = []) => value =>
    callbacks.length > 0
        ? callbacks[0](value).then(
                invokeCallbacks(callbacks.slice(1))
          )
        : Promise.resolve(value);

// or

const invokeCallbacks = (callbacks = []) => async value =>
    callbacks.length > 0
        ? invokeCallbacks(callbacks.slice(1))(
                await callbacks[0](value)
          )
        : await value;

// and then you use it like this:
const invoke = invokeCallbacks([/* Array of callbacks */]);
invoke(/* Initial value */).then(output => {
    // Do whatever you want with the output
});

// or
const output = await invoke(/* Initial value */);
// Do whatever you want with the output
Enter fullscreen mode Exit fullscreen mode

You might have noticed that I removed the logic to check if the callbacks are functions, that can be achieved with other function, and then just use it:

const isFunction = value =>
    typeof value === "function";

const invoke = invokeCallbacks(
    [/* Array of callbacks */].filter(isFunction)
);
Enter fullscreen mode Exit fullscreen mode

A few things worth mentioning of the above solutions:

  1. The function was turned into a curried version of itself, this means that all functions have an arity of 1 (they take just 1 argument), and they return a new function that takes the next. This allows us to reuse the same array of functions with different initial values :D
  2. If you need to deal with an array of promises in the real world, ideally you shouldn't do what I did, and you should use stuff like Promise.all, Promise.allSettled and so on. Promises can be quite powerful if we don't restrict ourselves to just use async/await.
  3. There are ways of writing this functions using async/await and they are still keeping data immutable. No need to keep updating that space in memory when we can pass stuff around.

One last thing that I recently mentioned in a stream: Don't default to Promises/async/await, try to think first if you actually need that function to be asynchronic (like if it needs to fetch, or read a file or something like that), and if it does then make it async, but if not then keep it as a regular function. In my years in web dev I saw stuff like this, that didn't make any sense at all:

const add = async (number1, number2) => await (number1 + number2);
Enter fullscreen mode Exit fullscreen mode

Thanks for commenting and the fun exercise! :D

Collapse
danbamikiya profile image
Dan Bamikiya

Thanks for taking the time to reply! I like how you're using recursion to solve this problem I also agree that a simple Promise.all would be best and simpler to read in a real world software. :)

Collapse
5ar profile image
Petar Kovačević

I agree that mutating should not be the default (when coding through multiple scopes), and that side-effects should be avoided at all costs. However, mutating within the same scope - e.g. changing an array crated in that scope by calling push on it - is and should be the default in a language that isn't immutable by default and optimised for it. Over-relying on the garbage collector can have an incredible toll on the overall app performance in the long run.

As an example let's take these two reduce cases for building a dictionary by id:

const usersById_v1 = users.reduce(
  (dict, user) => ({ ...dict, [user.id]: user }),
  {}
);

const usersById_v2 = users.reduce(
  (dict, user) => {
    dict[user.id] = user;
    return dict;
  },
  {}
);
Enter fullscreen mode Exit fullscreen mode

Both do the same thing, do not cause any side-effects, and result in a completely new object. But the v1 just has a completely unnecessary O(N) memory complexity that can really add up if this code is run often enough, and for no real benefit other than following the "no mutations ever" principle.

I know that this was not the point of your article but saying that you don't need mutation, and can rely on the garbage collector to pick everything up, can have a major impact on app performance and user experience in the long run. I've done metrics on stuff like this and it really adds up when something like this example becomes your default coding style

Collapse
lukeshiru profile image
LUKE知る Author

The thing is that in your example at least, you don't need reduce at all, there are better tools to loop over objects or to create objects from arrays (Such as Object.entries and Object.fromEntries).

Nonetheless your point was about local mutation, which I can agree to a certain degree, but you need to take into consideration that it might lead to accidentally do a push in an array that you received as a property of an object, effectively doing an accidental side effect that you could avoid.

I'm not saying you should never mutate, just saying that generally that "solution with mutation" can be done without mutations, so we should try to default to it and use them only if we really need it.

Collapse
5ar profile image
Petar Kovačević

The example was just there to illustrate the point, although I don't see how to create lookups directly with those methods. But that's irrelevant since these instances can always be extracted and used as a library.

In any case, yes, mutations of function arguments or in general any data linked to an object outside the function's scope is a side effect, and something that a programmer should keep an eye on. In practice I use TypeScript and tend to mark a lot of stuff as readonly as an extra check for these situations, but the main way of handling accidental side effects is code reviews.
There are a lot of tools to help with this, but honestly I've seen a lot of coders "hacking" the tools that should have caught something, and writing code that looks mutation free, just to miss one level of object spreading and assign a new value to an old object. Unless you basically write an immutable JS superset and use only that, no amount of coding style is gonna help you with avoiding accidental side effects 100% of the time. In fact, a "mutation free" coding style can cause people to be less vary of some edge cases with time and miss more things.

All in all, a tricky thing to balance, I just wanted to stress that there is no need to overdo immutability for something that only exists, or is just created in a local scope. Your point of mutations being something that should be used sparsely and carefully is definitely something I agree with.

Collapse
valeriavg profile image
Valeria

I think title "You don't need side effects" would be more precise.
Mutation, on it's own, is an important tool and allows to do operations simpler and more effectively (the garbage collectors are not that great), but it needs to be used wisely.
Small enough scope and absence of side effects makes mutations absolutely harmless

Collapse
lukeshiru profile image
LUKE知る Author

The idea with this article is to encourage defaulting to immutability, and if you really really really need a mutation, then do it, but generally you don't actually need it, is just what you're used to. I used pure functions in the examples, but the idea is to also avoid mutations at a top level, kinda like doing:

const obj = { foo: "foo", bar: "bar" };
const copyButNotQuite = obj;

copyButNotQuite.bar = "baz";

console.log("Copy bar is", copyButNotQuite.bar); // "baz"
console.log("Original bar is", obj.bar); // oops! This is also "baz"
Enter fullscreen mode Exit fullscreen mode

Mutations make our code more unpredictable, ergo harder to maintain and understand (even with experience, in the above example one might expect the copy to not affect the original, but it does).

Will you never need mutations? No. There might be scenarios in which a controlled mutation might make sense, but generally you can achieve that thing you want to do without any mutations, so why not default to an immutable approach?

This same logic applies to all the articles in this series (this one, the one about classes and the ones coming in the future). The previous article was about why you not need classes, and the point was the same: We shouldn't default to classes. They might be useful in some contrived scenarios, but we should avoid them and try to think solutions without them.

Collapse
dannymcgee profile image
Danny McGee

Mutation is at the core of the vast majority of bugs I had to deal with in my career

I'll do you one better: 100% of the bugs I have ever encountered were due to writing code, which, as we all know, is no longer considered a best practice. The one simple trick to completely eliminate every class of bug from your projects, once and for all: put away the keyboard, turn off the computer, go home, take up woodworking. Easy peasy!

Collapse
lukeshiru profile image
LUKE知る Author

Nah, I'm quite happy coding, even if I had to deal with mutations, side effects and classes all over the place 🤣 I know there are some folks in the industry that don't actually like to code, or they find it extremely stressful, so I would understand if they just quit, but getting out is not for me.... besides I bet that if I had to do woodworking, I would suck at it 😅

Collapse
kalashin1 profile image
Kinanee Samson

Mutating things cause all headaches, but if you store different things with different values as @codenameone just emphasized, you should have like 50% of your headaches sorted out.

Collapse
codinghusi profile image
Gerrit Weiermann

Hey, great article!

(Discovered a small mistake:
At the chapter "things to avoid from vanilla" at the end of your code you wrote addValue, but you actually need addFooBar :)

Collapse
lukeshiru profile image
LUKE知る Author

You're the best! Thanks!

Collapse
siddharthshyniben profile image
Siddharth

If you really want lots of purity, why don't you try wheeljs?

Collapse
lukeshiru profile image
LUKE知る Author

lol. Because using functions for every value is not what the functional approach is about, but I get that it might be hard to grasp for some folks that are used to have everything in classes 😅

Some comments have been hidden by the post's author - find out more