2017-03-05

Immutability as a secret test of character

Despite the fact that there is so much awe and worship of immutability in JS's FP's part of community, my beginnings with the concept were not at all related to functional programming.

It started with ESLint and no-param-reassign rule which guards against redeclaring a param. Those were my first steps with ESLint and I went with very strict approach in order to observe in the process which of them I really need. If I were filtering them out or found annoying, they would have to go. (More than six months later I dropped a number of them; some due to co-programmers not using ESLint.) No-param-reassign highlighted nicely and soon after I started using it, I met just one situation that convinced me that it's the right way.

The spark

So, picture this,

function letsWasteParam(array, getOriginal) {
    for (var i = 0; i < array.length; i++) {
        array[i] = array[i] * 2;
    }
    
    if (!getOriginal) {
        return 'Doubled array: ' + array;
    } else {
        return 'Original array: ' + array; // oops
    }
}

Whatever this function was meant to do is of no relevance here. The point is array is lost after first loop. Sure, a number of improvements could be done here (i.e., checking condition first, or creating separate array for new values) but basically something like that, just more imperatively elaborate, happened to me. I needed to use original array again but it was forever gone. And that's when I knew.

The name

After immersing myself — which is the only way to do anything — in the world of functional programming I discovered that what I learned by myself has a name. It's immutability. It means you do not mutate variables. You don't change their value, you don't reassign them. They are sacred.

(Being non-English native speaker, I still connect word mutation with "Teenage Mutant Ninja Turtles," so whole subject has like +5 to coolness for me. But that's just by the way.)

The way

Functional languages like Haskell have it by design. In Haskell variables are not even called variables — they are declarations. If you wanted to change them, BANG!, no way, dude. This aggression will not stand. JavaScript, being much more flexible, doesn't have such defence mechanisms.

But who's to say that one can't write in this manner? ESLint already helps with params, and outsourcing as much as possible to pure functions which should be as concise as possible makes it easier to control variables. Once you set it, leave it. It's that simple.

And thus the title of this story, as immutability in JS is possible but requires discipline, cautiousness, and character.

Let's run some examples now.

The default of primitive

A question of default value:

// ES5
function safeGuardES5(x) {
    x = x || 7;
    
    // magic
}

function safeGuardES5immutable(paramX) {
    var x = paramX || 7;
    
    // magic
}

// ES6
function safeGuardES6(x = 7) {
    // magic
}

Function safeGuardES5 is a common way to run with it. The workaround for the sake of workaround — "For sport," as we would say in my native language — would be safeGuardES5immutable which leaves param passed to function untouched. Mind you, at this point we're actually doing it for sport, exploring some ideas to see where they'll take us. The practical use is up the the readers.

There is safeGuardES6 function which uses native default value which does not produce extra variable but in case we wanted the original one (being undefined but still), we wouldn't have it. Though, I don't think it happened to me, hence widely acceptable practice of overwriting undefined parameter. Acceptable but mutating.

For more than two params I use object and that allows me to do this:

function doingSomething3D(params) {
    var x = params.x || 0;
    var y = params.y || 0;
    var z = params.z || 0;
    
    return 'x: ' + x +', y: ' + y + ', z: ' + z;
}

function theNewBlack({ x = 0, y = 0 , z = 0 } = {}) {
    return `x: ${x}, y: ${y}, z: ${z}`;
}

The tricky arrays

Primitives got covered. But what with arrays? Let's start with with real life example:

function ArrayContainer(array) {
    this.array = array;
}

// before
ArrayContainer.prototype.getNewOrder = function (startAt) {
    if (!startAt) {
        return this.array;
    }
    
    return this.array.splice(startAt, this.array.length - 1).concat(this.array);
}

// after
ArrayContainer.prototype.getNewOrderFP = function (startAt) {
    if (!startAt) {
        return this.array;
    }
    
    var array = this.array;
    
    return array.slice(startAt).concat(array.slice(0, startAt));
}

Section before shows code I found and after the code I left. Now, this is quite interesting situation here. Let's follow getNewOrder flow:

If there is no startAt, return array. Otherwise,

  1. Cut out piece of array from startAt to the end of array.
  2. splice returns the piece that was cut out.
  3. Concat this piece with the rest of array.

Works but only for the first time, the second time this.array is only the first part of original array. The tricky part of arrays in JavaScript is that they're instances of Array object. They are special cases of Object. And as such they are passed to function as a reference to an actual object hanging somewhere in the memory. Once it's changed inside the function, it's changed everywhere. So, in reworked version, I go with slice and concat which leave original object intact.

(splice can be very dangerous because it mutates object and returns part that was taken while leaving original irreversibly crippled. But this is a tale for another story.)

And this is where immutability becomes something more than just stylistic matter as it was mostly with default value. With our original function we were mutating object (array) that was used in other methods as well.

The objects

ES6 brought us const which prevents variable (now constant) from reassigning and primitives are by design immutable but what with objects? Example above with Array showed the danger of it.

Array has much more methods that would allow us to control a situation. With array.slice(0) we can create a shallow copy of array. And object? There is Object.assign() but again, as all objects in JS, it's tricky:

const object1 = {
    name:    'John',
    surname: 'Carbonlover'
};

const object2 = Object.assign(object1, {
    name: 'Frank'
});

// object1.name is now 'Frank', the same like object2’s

const object3 = Object.assign({}, object1, {
    name: 'Jeordie'
});

// object3 is a new object

In other words, watch your fingers.

We can freeze object but again, this affects only first level. Like that:

const cantTouchThis = {
    id:   1,
    type: 'fromage', 
    items: {
        knife: 'bowie',
        fork:  'krautrock'
    }
};

Object.freeze(cantTouchThis);

cantTouchThis.id = 2; // haha, not!

cantTouchThis.items.spoon = 'tarmac'; // oops

Would have to Object.freeze(cantTouchThis.items), as well. And then go even deeper

Generally, working with objects is more complicated and I tend to avoid actions other than creating them and reading from them, and that might be the best approach.

Conclusion

My goal today was not to present some ready to use recipes and solutions but rather to hint a philosophy of seeing things in code with certain aspects in mind. There are tools to help us catch problematic situations (ESLint), there are libraries (like Facebook's Immutable.js), and there are whole languages that compile to JavaScript (I'm looking at you, Elm), and using them is fine. Whatever optimizes your work is worth using it. But I want to tune my mind to see through those situations myself, instinctively. Done right in a first place will save me trouble later.