2

I'm learning about immutability (specifically in C#) and I can't understand the examples out there stating that making an object immutable brings real benefits (besides a shared object in a multi threaded environment, which the benefits are pretty clear).

The most talked example in my readings is DTOs. It's said that DTOs are a great example of a use case where the object should be immutable, but the reasons of it are not explained (or at least I couldn't understand). Okay, DTOs shouldn't change, but what are the benefits from enforcing that those types are immutable? Is it just that by making a DTO immutable, we are being more descriptive about the role of the object in the system? Does it goes beyond that?

7
  • 1
    "It's said that DTOs are a great example" - one problem there is that people will read something somewhere, and than repeat it elsewhere, without understanding the "why" behind it themselves. The other is that DTO actually means one thing (a flattened representation of data meant to reduce the number of remote calls), but many people use it to mean a different thing (database entities). It might could sense to make a DTO, in its first meaning, immutable on the receiving end of a request, but that's not really a "great example" (in the sense that it's not particularly illuminating). Jul 10, 2021 at 11:06
  • 1
    "There are only two hard things in Computer Science: cache invalidation and naming things" (skeptics.stackexchange.com/q/19836/3021). Immutability fixes the first one. Jul 10, 2021 at 22:55
  • Some beside the obvious thread-safety include: undo systems, non-destructive editing, and exception-safety. Undo systems are trivial to implement if you just store an entire copy of your application state. However, that can be explosive in memory use and processing if your data structures aren't persistent. Exception-safety is also difficult if you mutate your application state in functions that could fail halfway through, potentially leading to a half-baked application state unless you painstakingly roll back the side effects of the half-succeeded function...
    – user379844
    Nov 11, 2021 at 11:15
  • But by favoring immutability and pure functions like one that returns a new mesh rather than modifying an existing one in the scene for a computer graphics software, it becomes trivial to be exception-safe since if the creation of the new mesh fails halfway through, it can simply be discarded on stack unwind without affecting and potentially corrupting your central application state. So many things become easier when you reduce application state mutations to the bare minimum if you can afford it from a productivity or efficiency standpoint.
    – user379844
    Nov 11, 2021 at 11:17
  • That said, you don't have to design immutable classes and persistent data structures to a zealous level in an imperative language like C# to reap the lion's share of the benefits. The ultimate focus is to keep as many functions/methods pure and free of side effects as you can. If they mutate local, temporary variables/objects inside, then I think that's a practical compromise in a language designed to be mostly imperative.
    – user379844
    Nov 11, 2021 at 11:20

5 Answers 5

2

Like most "functional" concepts, this one has been over-hyped.

At some point, in almost all programs that are useful in a business context, something must mutate. Doing so in a correct and disciplined way is part of the art of programming.

In terms of benefits, you identify the obvious case of concurrent access to a shared object. What's not clear is why the shared object is being mutated at all, if it isn't necessary to do so. Most programmers instruct mutations on purpose because the mutations perform a necessary function, not by a slip of the keyboard.

If it is necessary to mutate, then making everything immutable is no solution to the problem. It just pushes the problem around to another area of the program, where eventually something (perhaps even, a lot of things) must in fact mutate.

And whatever was to be gained by immutability, will perhaps be lost again by unintentionally holding expired data, which is the mirror image of unintentionally updating shared data.

If there is any grain of truth about immutability, it is probably this. Mutations to "master data" (and exactly what that is may differ from program to program, but it is typically the part which is stored indefinitely) should be planned out, retaining all intermediary values until the completion of the operation (i.e. don't reuse variables and objects for multiple steps of a calculation), and then the final changes to the master data should be applied "at once" (i.e. in a relatively localised area of the program).

This contrasts to "worker-bee" type approaches where there are many subroutines (perhaps on multiple threads) applying many incremental changes all over the master data, and which have often already thrown away intermediate values for one change before they have have even decided/calculated what the next change will be.

The widespread use of immutable objects, where any necessary mutations of shared data must always be pushed to a relatively high level in the program/call stack, make this worker bee approach prohibitively difficult from the outset. Other things can still go wrong, but it helps that problem at least.

2
  • "final changes to the master data should be applied "at once" (i.e. in a relatively localised area of the program)" is the idea of Clojure's Atoms.
    – closeparen
    Jul 10, 2021 at 22:36
  • Also related: "functional core, imperative shell"
    – closeparen
    Jul 10, 2021 at 22:36
4

Consider testing. When you test a function that mutates some internal state, you have two headline options.

One is to assert a specific mutation. This might be accomplished through a mock expectation for some upstream stateful object, or through reading the value of private instance variables. Either way, the test becomes tightly coupled to the implementation. It will be very difficult to refactor the method without upending its unit tests, which defeats much of the purpose of having them.

Another is to instead verify that the mutation has the desired effect on future behavior. For example, after I place an element into a collection, I should be able to read it back. This option better respects the privacy of the collection's implementation. It gives the class under test more latitude to choose its internal representations, as long as it exhibits the behaviors I check for. The problem is it could do literally anything to the behaviors I don't check. For example maybe my linked-list insertion is buggy, and inserting an element will cause the list to lose a different element.

Pure functions don't have this problem. I can assert that when I call a function with a certain input, it returns a certain output. And that's it. That's the only thing it does. As long as I assert the whole identity of the value returned, there can be no surprises about other consequences of having run the function.

1
  • Interesting, but how this answers OP's question? I can use mutable objects as input and output of the pure function and test that returned value is as expected.
    – Fabio
    Jul 13, 2021 at 18:58
2

The biggest advantage of making objects immutable is that it makes it easier to understand and reason about the code.

Even with single-threaded code, you don't have to consider the possibility that a function you call might change the value of the object.

For a 2000 line program, this is not really critical, because the program is small enough to be able to follow all the modification it makes to objects. But as programs grow larger, that size increase makes it harder to understand what the code is exactly doing if if that is the correct thing. Then (compiler diagnosed) immutability can help you free up mind capacity to think about other things that need to be correct.

8
  • 2
    FWIW, there's plenty of questions on Stack Overflow where people get confused by mutability in 4-line programs. Erik Meijer, who worked on the design of Visual Basic.NET, LINQ, .NET Reactive Extensions, Haskell, Hack, Dart, RxJava, and many other things, famously said that the first time he saw the statement X = X + 1 in his programming classes, he was utterly confused because there is no X which is equal to its own successor, so even very smart people can be confused by mutability in a one-line program. Jul 10, 2021 at 9:38
  • 2
    @JörgWMittag that's confusing assignment with equality. Which is why in some languages they use := for assignment. Immutable objects help readability by ensuring there are no reassignments. Jul 10, 2021 at 14:24
  • Correct me if I'm wrong but should I assume you are suggesting that in every big project (LoC-wise) objects should be designed as immutable whenever possible? Jul 10, 2021 at 21:26
  • This answer addresses constant arguments. Immutability typically describes something different, on the type level. Like "in C# strings are immutable objects." Jul 10, 2021 at 22:55
  • @MartinMaat how is C# string's immutability different from what he is talking about? Jul 10, 2021 at 22:59
2

Immutability makes memory management a lot easier. Rather than messing with an existing object to change it and synchronizing shared access to it, a new instance is created and the old one is discarded. This can make things less efficient from a performance point of view but it also makes things easier and more robust.

Creation logic is needed anyway so it might as well be reused for assignment logic. If a change requires reallocation of an object, you might as well recreate it with any change, always, and be done with it,

0

In my experience immutability usually had 2 major benefits.

1) When used consistently throughout the whole program, it gives extra information about the code, for the next developer working on it.

If a local variable is mutable (even if it was not supposed to be), the next time someone modifies that piece of code, they might not spend the time to evaluate if that was meant to be mutable or not. But if you declare it immutable (for example writing out const or final), that will make the next developer working on that code assume, that you meant it to be immutable, making it mutable and changing its value might break something somewhere.

2) In compiler construction, there is a technique called "constant folding". In simple terms this roughly means, that if the compiler understands that some operation is done on immutable parameters ... it can decide to calculate the resulting value in compilation time and generate that into the executable instead of the operation itself. (for example instead of generating code for x = 1+1, the compiler could just generate code for x= 2 ... this is faster to perform during execution)

Building on this you can write code, that is expressive in its source code form, but does not incur any actual operation during execution.

That said, compilers also have to balance how much effort they put into this kind of optimization: if they don't do constant folding aggressive enough they can leave performance improvements on the table, but the more time compilers spend on finding more situations to optimize the longer the compilation takes.

Immutability can be like offering a helping hand to the compiler. If you have a long and complex function, identifying if a local variable could be immutable might be too costly for the compiler to perform. But if you declare that variable immutable the compiler can save all that hard work and find some places where constant folding can be used.

Not the answer you're looking for? Browse other questions tagged or ask your own question.