DEV Community

Cover image for Mutates or not? We need both versions but there is a problem.
Michał Kuncio
Michał Kuncio

Posted on • Originally published at michalkuncio.com

Mutates or not? We need both versions but there is a problem.

The problem

In Javascript, arrays are without a doubt one of the most broadly used data structures. That's why we need as many built-in methods as possible to manipulate the arrays the way we want. Javascript evolves and almost every new ES specification brings us some new array methods making it easier and easier to work with arrays. One of my favorite new ones is Array.prototype.at which makes getting the last element of an array so simple!

Instead of

const lastElement = array[array.length - 1]
Enter fullscreen mode Exit fullscreen mode

we can do:

const lastElement = array.at(-1)

Enter fullscreen mode Exit fullscreen mode

The problem with array methods is well known to every developer. The thing is, some array methods mutate the original array but some don't. I challenge you to guess if Array.prototype.slice mutates or not? And what about Array.prototype.splice? I can check it every single time I use one of those methods. And even if I use other methods that I am almost sure to mutate, I still check it just to be sure. If someone doesn't know Does it mutate, it's a great resource to reference array methods.

But having to remember whether the array method mutates or not it's not the only drawback. What if there is a need to use one of the mutating methods like Array.prototype.sort but without changing the original array? We need to copy the original array and then apply the method to a cloned array. And what if we want to apply an immutable method like Array.prototype.filter but instead of creating a new array we want to filter the existing one?

Luckily there is some noise about that in the Javascript world. So let's take a look at one of the proposals.

The proposal

TC39 committee came up with an interesting proposal that introduces immutable versions of mutable methods.
Change Array by copy.

And being more specific we talk about reverse, sort, splice and at.


    Array.prototype.withReversed() -> Array
    Array.prototype.withSorted(compareFn) -> Array
    Array.prototype.withSpliced(start, deleteCount, ...items) -> Array
    Array.prototype.withAt(index, value) -> Array

Enter fullscreen mode Exit fullscreen mode

Now, let's take a look at some examples and see how these immutable versions behave.

const sequence = [1, 2, 3];
sequence.withReversed(); // => [3, 2, 1]
sequence; // => [1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

As we can see, applying withReversed methods return a new reversed array, without modifying the original one. By that, with this method, we no longer have to clone the original array manually.

Same principle applies to withSored and withAt:

const outOfOrder = [3, 1, 2];
outOfOrder.withSorted(); // => [1, 2, 3]
outOfOrder; // => [3, 1, 2]

const correctionNeeded = [1, 1, 3];
correctionNeeded.withAt(1, 2); // => [1, 2, 3]
correctionNeeded; // => [1, 1, 3]
Enter fullscreen mode Exit fullscreen mode

Both of them return new arrays without modifying original ones.

Everything looks clear and these kinds of immutable methods would be useful. So, what's the problem?

The problem with the proposal

Well, it's not a problem with the proposal itself. But if we have immutable versions of mutable methods it would be great to have mutable versions of immutable ones?

For example what if there is a need to filter array out of specific items without creating a new array and allocate a new memory block?

By now we have to do it that way:

const numbers = [24, 41, 12, 7, 4, 50];
const greaterThanTen = numbers.filter((number) => {
    return number > 10;
})
Enter fullscreen mode Exit fullscreen mode

By doing that I have an original array and a new filtered one. I can imagine specific needs when a new array is not necessary at all.

So how to approach that kind of problem? How to name those mutable methods?

Solution?

If we would take Change Array by copy proposal by a starting point we would have a naming pattern like that:

    // Reverse
    Array.prototype.withReversed()

    // Sort
    Array.prototype.withSorted()

    // Splice
    Array.prototype.withSpliced()

    // withAt
    Array.prototype.withAt()

Enter fullscreen mode Exit fullscreen mode

In this case with modifier makes mutable method immutable.

So how to approach the immutable method to make them mutable? What kind of modifier or keyword would be appropriate? First of all, we have to consider if this pattern (withSomething) is intuitive? To be honest for me, it's not the clearest way to communicate that this method is immutable. Are there any other ideas? Sure. Let's take a look at these examples:

Idea 1

Immutable -> Mutable

// Filter
const numbers = [24, 41, 12, 7, 4, 50];
numbers.filter((number) => {
    return number > 10;
}) // mutates

// Flat
const array = [1, 2, [3, 4]];
array.flat() //mutates

Enter fullscreen mode Exit fullscreen mode

Mutable -> Immutable

// Filter
const numbers = [24, 41, 12, 7, 4, 50];
const numbersReversed = numbers.reversed(); // doesn't mutate

const numbers = [1, 30, 4, 21, 100000];
const numbersSorted = numbers.sorted(); // doesn't mutate
Enter fullscreen mode Exit fullscreen mode

Explanation

This concept assumes that applying the method in imperative form like sort, filter, reverse, etc would always modify the original array. It's closer to natural language because we can read it as "Let's take numbers array and sort it". On the other hand applying method in pasts forms like sorted, filtered, reversed would return a new copy of the original array. We can read it as "Let's return a new array with sorted items based on numbers array. For me it's almost perfect and intuitive. Where is the catch? Because it's always a catch isn't it?

This approach has a really serious drawback and its...
Backwards compatibility.

This approach assumes that the behavior of existing methods should be changed. By doing that it would break all existing applications so unfortunately, it won't happen... ever. Let's find another solution.

Idea 2

This idea introduces copy modifier making it easier and more natural

const numbers = [24, 41, 12, 7, 4, 50];
const numbersReversed = numbers.copy().reverse(); // doesn't mutate

const numbers = [1, 30, 4, 21, 100000];
const numbersSorted = numbers.copy().sorted(); // doesn't mutate
Enter fullscreen mode Exit fullscreen mode

It's not exactly a new immutable method name but rather a chainable array modifier that acts as a copy helper. So it's not exactly the ultimate solution but would be nice to have in future versions of JS anyways. The benefit of this copy helper method is that it doesn't break anything because the naming of array methods would stay the same.

Conclusion

We have analyzed three concepts of making it more clear if the method is mutable or immutable. As we saw it's not easy because it's easy because it is either unintuitive or breaks backward compatibility or is a half-hearted solution. I'm sure people would have more interesting ideas on how to solve this problem and I'm really curious about the final shape of this proposal.

Discussion (4)

Collapse
lukeshiru profile image
LUKESHIRU

I have a "strong opinion" against mutation, but still, this comment is not based on that:

Your idea is not possible because, for example, Array.prototype.flat returns a flatten copy, and will always return that. One of the rules of JS is "don't break the web", so methods that have a behavior, will keep having that behavior, even if it doesn't make much sense. Take null as an example: We all know by now that in JS typeof null returns "object", but the fact is that it should’ve returned "null" but the first implementation of JS had a bug on a switch case that they use for the typeof function, and they messed up returning "object", and now it can't be changed without "breaking the web". Same applies to everything, so the only option is to add new stuff, not to change the stuff that's already there like Array.prototype.flat or Array.prototype.filter.

I would love the exact oposite to happen, and for example get Array.prototype.reverse to be changed so it doesn't mutate, but sadly we can't, so that's why we are getting Array.prototype.withReversed instead.

Cheers!

Collapse
michalkuncio profile image
Michał Kuncio Author

Hi! Yes, that's why I mentioned that the best idea for me wouldn't be really possible because of backwards compatibility. I guess for now the solution from TC39 proposal is the most realistic option we have. Cheers!

Collapse
jonrandy profile image
Jon Randy

With my project 'Metho' - we can easily safely add 'dynamic properties' to the Array prototype that could do what you are suggesting. We could make something like the following:

const arr = [2, 1, 3]
console.log( arr[reversed] )  // [3, 1, 2]
console.log( arr[sorted()] )  // [1, 2, 3]

const descending = (a, b)=>b-a
console.log( arr[sorted(descending)] )  // [3, 2, 1]

console.log( arr )  // [2, 1, 3] - no mutation occurred

// etc...
Enter fullscreen mode Exit fullscreen mode
Collapse
jonrandy profile image
Jon Randy

I'm actually planning a metho-array library, so I'd probably include all/most of the above in that