Ross Esmond

Code, Prose, and Mathematics.

portrait of myself, Ross Esmond
Written — Last Updated

Advantages to immutable values

The primary advantage of immutable values is that they separate computation from commitment. The code that updates a mutable value destroys the prior state of such value and immediately replaces the value with the new state everywhere that a reference to the value is held. Conversely, a function that computes the new state of an immutable value does so without the program committing to that new state, allowing the program more flexibility in how it uses the new and old value.

Computation of new values will often first be created with the express purpose of committing to the new value, as new features are most valuable when they’re consequential. But the separation of computation and commitment will quite often become necessary to implement important features later that involve running existing computation code without committing to the changes right away. The ability to reuse computation without commitment—a direct consequence of using immutable values—makes functional programs more flexible.

Separation of computation from commitment allows a program to hold historical values, generate hypothetical values, and change its imperative strategy of handling the commitment.

Historical Values

Historical values are those that have been replaced by newer variants, such that the historical value is considered to be outdated. It may still be useful, however, as historical values may still be relevant to the operation of the program.

The most obvious reason to keep historical values is to retain the ability to roll the program back to a prior state, which may occur due to a manual rollback by the user or an automated rollback due to some system trigger. Historical values may then be used to introduce Reversibility to an application, which almost every user action should have.

Historical values may also be used to determine the status of the program in terms of some changing state. The CPU usage 60 seconds ago may not be up-to-date now, but the change in CPU usage over the last 60 seconds is an up-to-date value, and therefore the historical value it finds relevance.

If a value is mutated directly, it becomes difficult to maintain historical values. This is usually achieved by either updating the value to be immutable, (along with every operation on that value to match,) or by creating a copy of the value, as with the momento pattern.

Hypothetical Values

Hypothetical values are newly computed values to which the program has not yet committed. Sometimes the program will commit to these values later, and other times they will be used for entirely new purposes, with no chance of commitment.

A hypothetical value may be created to provide feedback to the user before commitment, possibly as part of a confirmation step, or in a more integrated fashion. A hypothetical value can be run through validation before commitment, which may be exceedingly difficult to perform when the value is mutated in place. Hypothetical values may then be used to introduce an Intentional Impediment to a consequential user action.

Since the hypothetical value is distinct from the current value, both may be diffed against each other in order to determine precisely how the values have changed. This, of course, may be used for even better validation or feedback, or to improve the performance of any updates that may need to take place as a consequence of the change. The simplest performance improvement is to simply not trigger an update if the value ultimately didn’t actually change, or if the changes are confined to some inconsequential range.

If a computation mutates values in the creation of new states, it cannot be reused to create hypothetical values, and the computation will need to be rewritten if you find a need for a hypothetical application of that computation. You could keep two versions of the computation—one consequential, one not—but you will then be forced to maintain two variants of the same computation, and if you forget to update one, you create an elusive bug. For this reason, it is better to simply update the computation to treat the value immutably.

Choosing an imperative strategy

Ultimately, consequential state changes must take place. Even the most functional languages have some mechanisms to maintain program state and react to state changes. There are many ways to handle state changes and their down stream consequences, however, and immutable values allow you to mix-and-match computation with different imperative strategies, such that a functional program can often be rewritten to follow the best approach.

Take React-js as an example. React creates a state object for each component, which may be updated by calling setState. When the state is updated, React reruns the components render function to determine which changes need to be made to the components interface. This structure is different from reactive values, as seen with RxJS, but both of these libraries can use immutable values, as the commitment to a new immutable value is not baked into the computation for that value, allowing the commitment to take place however the programmer chooses.

With mutable values, imperative strategies must often be built into the mutable values themselves, as a separate system will not be consulted on consequential updates. A mutable value may change without a framework knowing, alienating that framework from such an update. In Object Oriented programming, this may be alleviated with the Observer pattern, but that assumes that both the framework and the computation agree on the observable interface, which is rare.

Use of Immutable Collections as Keys in Maps and in Sets (performance)

For instance, with memoization.

Use of Immutable values in multithreading

Clean up after an error

With mutable values, if a function fails half-way through editing a collection, that collection may be left in an impossible state, corrupting future operations involving it. With Immutable values, the value is swapped out all at once, such that a failure half-way through computation will only result in the update not taking place. The update still failed, but the overall state of the application is not corrupted.

Pure functions and the Generalized Open-Closed Principle

“Side-Effects” of functions are specified in the signature