Mutation means to be able to change an object’s form, behavior or nature. Something that’s mutable can be changed, while something that’s immutable cannot.
According to Wikipedia, a side-effect a function or expression is said to have a side effect if it modifies some state or has an observable interaction with calling functions or the outside world.
Another way to think about side-effects is to view it as a piece of code whereby a variable is created and available (i.e. can be changed) throughout a scope when it doesn’t need to be. In other words, a side-effect occurs when a program / function modifies the state of something (e.g. variable, object) outside its own scope.
This could effectively include something like “display a character on the screen”, or it could be changing the value stored in some arbitrary RAM location, updating a database row or anything similar.
Can side-effects be always avoided?
Some of those things are bad programming practices (e.g. modifying global variables, or worse adjusting a value saved in RAM at some arbitrary location) while others are near impossible to avoid: Your program may not be able to “work” if it’s not “allowed” to change something outside its scope. For instance, updating a database row can be considered a side-effect, as it affects other parts of an applications, or worse, other applications that use the same database.
For this reason, most programmers ignore the cases where side-effects are unavoidable. They tend to only consider those which can be avoided as “practical” side effects, which should be avoided by preference.
To understand mutation, we can relate it to Marvel’s X-Men, where people that have a special gene, can suddenly gain powers. The problem is, you it’s not possible to know what these powers will be or worse, when these powers will emerge. This means that you can’t really plan something with these guys as they could suddenly become a monster and eat you or develop laser eyes, turn your food to ashes and leave you hungry.
Taking this to the programming works, to mutate a variable (after it has been defined) means to alter its value. But this doesn’t look dangerous, does it? The problem becomes apparent when what mutates can be accessed from different places (scopes). Let’s take a look at this.
Primitive data types are managed by value while the non-primitive/reference-based data types manage pointers to memory addresses. This means that whenever we change a primitive-type item, we create a new instance/copy of it (the original data stays unchanged), while updating non-primitive data-types changes the referenced object (a new object is not created).
For complex types, any change on the data will impact all occurrences because they are only pointers to the same place. Using mutable structures is a reason of the trust deficit. Mutation can be done in every code part with the access to the reference and will affect the rest of the code having access to the data.
Mutation and spaghetti code
The opportunity of mutation provides no guaranty that something will stay unchanged. The worst scenario takes place when some structure is used in separate parts of the application, because any mutation in one component can create a bug in the other one, leading to what’s called “Spaghetti Code”. Just like sticky spaghetti.
Following the spaghetti metaphor, when you mutate objects, often shared with other parts of the application, you see a number of changes in different places. You touch it in one place and you see other things get broken or modified in an unexpected way.
When a bug will be found the questions will be risen — Where (and when) it was changed? What exactly was changed? Who also have access to the reference? But no history of change is available and questions can’t be easily answered. Under these circumstances, code testing and fixing bugs is really hard, because the source of the problem lies out of the scope of the failure.
Another problem related to mutation is the lack of the knowledge about changes, which grows with number of places having access to the same mutable structure. It stays unknown how the structure is changing in the time. This lack of knowledge complicates testing and debugging even further.
Preventing Mutation-related problem
Do not change objects after construction
This is the simplest approach, and the weakest of all. It involves two main tasks for each piece of code. The first is to write functions that return altered copies instead of changing properties of the given object.
And the second is to (manually) avoid changing objects after construction. What does it mean? Objects are references, so if we avoid changing their properties we avoid mutation and we banish the situation of unclear state.
Needless to say, these two must are to be accomplished by hand so this is really prone to end up with involuntary errors, so extra tools and constructs are really needed.
Pure Functions (Ravioli Coding)
Unlike spaghetti code, ravioli code implies wrapping up each piece of logic in its own function. This ensures that anything outside the scope of the individual “ravioli” is not changed. Any changes to these will be returned as new objects. These raviolis are usually called pure functions.
In simple terms, pure functions are functions that accept input parameters and returns a value without modifying these parameters or any data outside its scope, avoiding any unwanted side effects. Calling the function many times, using the same parameters, will always yield the same results.
Most importantly, pure functions will not affect any other part of the code (they only work with the data from parameters or those created inside). This is (one of) the core principle(s) of functional programming. If I pass a value to a function, I can promise it stays the same forever.
Immutable.js is a library created by Facebook and it provides many Persistent Immutable data structures such as: List, Stack, Map, OrderedMap, Set, OrderedSet and Record. These structures return a new, updated instance for every change to the structure or content. This ensures that the original structure will not be affected and the other parts of the code will not be plagued by unwanted side-effects.
When immutability can get in the way
While immutability should always a high-priority task while developing software, sometimes this can get really badly in the way of performance. For instance, if you have objects and structures that change really often, it isn’t the best idea to create a new instance for every change.
This is especially true for games, cryptography or simulations where this happens multiple times a second. This does not mean to forget about immutability in these cases, but it’s way easier to fall into performance issues than with mutable objects.
Another likely potential pitfall of immutability is when huge data structures are involved. Making these structres immutable and copying them on each update can really affect memory consumption. Of course, memory is cheap these days and garbage collection does kill all old, unreferenced objects but copying big objects does still have its price.
In most cases, it’s a pretty reasonable software design choice to avoid mutating structures and stick to immutable objects, even when they don’t model nicely when everything in the system is immutable and every change requires making a copy of the data. In most situations, the advantages of immutable types vastly outweigh its disadvantages. Working with an immutable objects simplifies testing and debugging, which in turn leads to better code and more stable, faster and more optimized code.