This is a bit of an aside, less directed to a particular project and more directed to a particular question. One of my recent posts, Decomposing Composition, led to a someone asking that I perhaps talk about the dangers of the Array.prototype.reverse
function and others like it:
One more thing... I know you're not trying to re-create "Composing Software", but would it be worth addressing how Array.reverse() actually modifies the passed array, as well as returning a reference to it? Mutation! Mutation!
That's unlike the other JS functions, which just return something new.
And what implications does that have in a pipeline or composition?
Thanks, Greg! Both the question and what it implies has had me thinking some. And with that...
The Fairy Tale
The story is well-known: a certain little towheaded kid decides to take a walk in the woods, break into a home, raid the larder, destroy the furniture, and ransack the bedrooms. Upon being caught, she flees the scene.
Yes, I'm referring to Goldilocks and the Three Bears. In the story, a family of bears has gone for a walk, to allow their meal time to cool. While out walking, Goldilocks enters their home and samples each ones' food, and chair, and bedding - and in the process consuming or breaking each ones' things.
If you search "What is the moral of Goldilocks and the Three Bears," you might find this:
The moral of the story is the need to respect the privacy and property of others and how your actions hurt others. In conclusion, the story Goldilocks and the three bears illustrate the need to respect others' privacy and property. ...
(Taken from PEDIAA)
Now, that's a moral, and not a bad one, but my suspicion is that the teller of this story was a developer.
A Different Moral
It seems to me that the story illustrates a very important point: the bears went out, secure in leaving their stuff in their home. They trusted in the fact that, when they returned, their stuff would be exactly as they'd left it. Instead, the warning is a dire one: on looking at their possessions again, things have changed. In the case of the Baby Bear, things had changed disastrously.
As developers, we can see this as a warning about immutability. When we store a value into a variable:
const myName = "Toby";
We are doing a few different things here.
- We are placing a variable,
myName
, into the current execution scope's lookup tables; - We are placing a string,
"Toby"
, into memory somewhere; - We are "wiring" that variable to that value;
- By the
const
, we are telling that variable that it cannot be rewired.
So we have two different points to note:
- First, primitive types are immutable. You can't change them in-place. If we were to
toUpperCase()
that string, we would have a new instance, in a new memory location. The original cannot be changed. - Next, a
const
can only be declared at initialization. From that point forward, that reference is immutable. So not only can't the value be changed, the variable can't be changed.
This implies that we can trust that myName
will remain exactly what we've defined. Any time I call up on myName
, I will get the same value. In effect, we have placed it in a house and locked the house.
Consider a different case:
const myFriends = ['Therese','Daniel','Greg','Annika'];
We've done the same here. myFriends
is a const
now, so it will always point to that same array. All great and fine and wonderful... until we do this:
myFriends.sort((a,b)=>a.localeCompare(b))
console.log(myFriends);
// ['Annika','Daniel','Greg','Therese']
So we placed that array into a variable, a const
no less... but then we sorted that array. And Array.prototype.sort
is one of those pesky "in-place" array methods. We have mutated the myFriends
array.
In smaller simpler cases, that may not seem like a big deal. I mean, I made that array, and I then changed it - what's wrong with that?
The Problem
If I can mutate a thing publicly, can I trust that thing? Say we have an admin control panel we're building, and we're storing our data structure like that. That admin control panel might have a number of different controls, but to make it easy to work with, we just let them store their data in arrays, scoped to the AdminControlPanel
module. They're contained, so they're not contaminating the global namespace.
Imagine further that we designed the thing to be modular. Folks could create their own control panel components, and load them in as they like. When they run, they create their data-pools as needed within that AdminControLPanel
, they do their initialization, and they toddle on about their things.
But what if two of those components used the Friends
array? Say one of them allows me to add notes about those friends, while another might look up their contact information. Imagine that we load up the AddNotesAboutFriends
admin module, create the array, and even have a common object style between other things accessing Friends
that allow for extra details. Works great, loads all the records about our friends, and lets us add, edit and view notes we've made. Great!
Then we have the ViewFriendDetails
component. Given that we defined a uniform format for those friends, this one might let us search for friends, sort them to make it easier to find, and show a detail view for a selected one. Also works great, no worries.
But... what just happened? If our ViewFriendDetails
component sorts that array of Friends
and the AddNotesAboutFriends
was looking at that array? We may have broken the trust there. We can't rely on the Friends
array, because something outside of our control is now mutating that thing, leaving it in an unexpected and unreliable state!
Why Immutability Matters
We need, as developers, to work with data. We need to know that, when we look away from it, it won't be like the Weeping Angels in Dr. Who, sneaking about and changing position and doing dangerous stuff. We need, in short, to trust that data.
In that article I linked up top, I explored alternative ways to do the reverseString
lesson common to most programming curricula. One of them was this:
// some utility functions, curried.
const splitOn = (splitString) =>
(original) =>
original.split(splitString);
const joinWith = (joinString) =>
(original) =>
original.join(joinString);
const reverse = (array) => array.reverse();
const reverseString = compose(
joinWith(''),
reverse,
splitOn('')
);
Not going to explain it all, I think that last article did pretty well. But some of those small, simple functions are great:
-
splitOn
takes a string to use for our "splitter", and a string to split. From that, it returns an array. -
joinWith
does the reverse: it takes a string to use as our "joiner", and then joins an array of values into a string.
In both of those cases, as we're transforming that data, we have an entirely new thing. We aren't transforming the string itself in place, we are making something new.
Then we have the reverse
function. I wanted to write a simple wrapper, so I could simply pass in an array and flip it about. Rather than calling array.reverse()
, I wanted to be able to call reverse(array)
. But I lost sight of those implications.
"That reverse
function, in this particular case, really doesn't matter." We only use it on transitional data anyway, so the value ends up thrown away. So really, it doesn't matter that array.reverse()
isn't returning a new thing, right?
Wrong.
It matters. Why? Because I can't know the application of my functions. I have no way of knowing where that reverse
function may get used down the line. It is a great and useful function, it might pop up all over the place. The whole point of the "Functional Programming" concept is, we can create these small simple one- or two-line functions, and interconnect 'em. And they will work.
But in this case, array.reverse()
is Goldilocks. We have reached back to the original array reference and mutated it. Due to the way javascript passes values, both the original array and the one inside of the function are a shared reference. They both view the same memory location, and either can mutate that. This, folks, is a bad idea.
Why?
A key principle in Functional Programming is "purity". When we talk about purity, we mean that our functions should:
- Given the same input, return the same output, and
- Not cause side effects.
So, for that reverse
function, we get the same thing every time: when we pass in an array, the return value is the array reversed. But we have caused a side effect! We have mutated the original array, as well as returning it.
We need to be able to trust that, not only does our function do as intended, but that it doesn't do anything unintended. For example, altering the original array.
Simple Fixes
In this case, the fix is simple: rather than simply reversing the array, we want to reverse a copy of the array:
// bad:
const reverse = (array) => array.reverse();
// better:
const reverse = ([...array]) => array.reverse();
In that one, when we are receiving that array
, we immediately spread it into a new array. We no longer refer to the original, so when we array.reverse()
we are working on our own local copy. And when we return the value, the original array remains untouched.
With that version, regardless of where we use it, when it gets composed into other functions, we are creating a transformation rather than causing a mutation.
Other Gotchas
There are other methods and places that we need to be watchful. Here's a typical warning, taken from the MDN:
The
splice()
method changes the contents of an array by removing or replacing existing elements and/or adding new elements in place. To access part of an array without modifying it, seeslice()
.
(Bolding added for emphasis)
In the Mozilla docs, if we see an array method marked as in place, these are mutators. They will alter the original thing, if we are not careful. They will Goldilocks us, eating our porridge and breaking our chairs.
There are others. The use of objects for storing exposed data can be problematic, as that exposed data can mutate at any time and we have no real way of knowing. We can't trust objects, unless we are very careful and very explicit - they can very easily be mutated.
We can render them more trust-worthy, some of which I've written in other articles: rather than using a class
and creating an exposed object, use a Factory function, and Object.freeze()
the returned accessor.
The Point
The moral of the story is, to my mind, trust. We need to be able to store our data, and trust that it will be what we expect when we return to it. We can't do that with data that is publicly exposed and in a mutable structure. We need to plan, and we need to be preventative.
Top comments (0)