Hey all,
Why do you use immutabile objects, and states? What real benefits does it offer in your situation? Is there any feasible alternative that you could’ve implemented, if so what was it and why was it feasible?
Hey all,
Why do you use immutabile objects, and states? What real benefits does it offer in your situation? Is there any feasible alternative that you could’ve implemented, if so what was it and why was it feasible?
For further actions, you may consider blocking this person and/or reporting abuse
Ezpie -
Pratik Tamhane -
Rasmus Stjernström -
𒎏Wii 🏳️⚧️ -
Top comments (5)
A bit of comparison on the issues that led to immutability as a solution.
Let's start with procedural code, as it is the style that most closely matches the way hardware works and was first on the scene AFAIK. Procedural code triggers side effects on a global state and subsequently calls other code that triggers other side effects on global state. The big problem with procedural code is that each procedure has certain expectations on what global state is considered valid for itself and what can be done with it. Over time it becomes very difficult to remember all the different expectations of each procedure (in order not to violate them), and consequently very easy to create ripple-effect breakages in procedural code by changing a single procedure.
One answer to this issue is the OO paradigm, which proposes to carve off chunks of the global state and hide them (Encapsulation) in objects. The object itself controls access to the data and instead exposes higher-level operations (i.e. methods) that other objects can call. It is only these methods which are allowed to mutate the state. This prevents changes to external objects from rippling in and breaking my object.
A different formulation to solve the issues with mutable shared state is used by FP. It is a combination of things including immutability. Now we are using functions, which take and return a value, instead of procedures which are expected to perform side effects. We take in only data needed by the function instead of reading global state. When we need to "change" immutable input data, we copy it and include our changes in the copy. (This practice exists in OO as well for certain kinds of objects and is called "defensive copying".) This does not break anyone using the old copy of the state. There may still be a "global state", but it is only accessed at the very highest level of the program, not in the small. Each function only knows about its small piece. Consequently, it becomes easy to understand (and test) precisely what a function does in isolation of the rest of the system.
As stated, immutability is just one ingredient in the FP solution. But it may also be useful elsewhere on its own. I.e. in place of defensive copying, or as a half-solution to the code smell of "Inappropriate Intimacy". It is also handy for multi-threading since one of the really hard parts is managing access to shared data. If you are never concerned about coordinating writes or syncing data across threads due to writes, then multi-threading just got a lot easier.
For me, immutability means clarity and understandability. Consider the following Java snippet:
Looking at this piece of code alone, can you determine instinctively what is the state of
someObj
after all the statements have been executed?Simply put, you can't. In languages where mutability is the default (e.g. in Java), any method that receives an object pointer (reference) are free to do anything to the referenced object. And, speaking from experience, bugs related to this behavior has bitten me more than I want.
"But that should not be a problem with properly named methods!" Oh, but names can be misleading too. Going by the name alone,
doSomethingOnObj
is expected to somehow change someObj in one way or another, whiledoSomethingWithObj
is not expected to mutate. But language-wise, there's nothing stopping developers chased by deadlines to put mutating code in any of those methods. As such, you don't have any confidence on the state ofsomeObj
without digging to both methods' implementations and see where the mutations take place (which of course can be hidden in a chain of nested method calls).In immutable-by-default languages, such as Haskell, OCaml, Elixir, etc. such things are simply not possible to happen. After creation, entities are immutable, and you can only return a copy (with modifications) of those entities. Now, suppose Java is immutable, let's look at the following code:
After executing those statements,
someObj
is guaranteed to be the same as what it was on line 2. Line 3 is not possible at all to changesomeObj
in any way. To know what the state ofsomeObj
is, you only need to look at the implementation of doSomethingOnObj (which, hopefully, is properly represented by the function name), and you can be suredoSomethingWithObj
has absolutely nothing to do with it.Readers might argue that line 2 is mutating
someObj
; it's not. It's called rebinding. Elixir is one language that is immutable but allows rebinding, which prevents the use of intermediate variables. Consider again the following code:In this snippet, we are guaranteed that
doSomethingOnObj
will not in any way modifysomeObj
, because the return value is assigned to a different variable. Which definitely contributes significantly on how we can understand the code at hand.Hope it helps!
Immutability is a powerful idiom. Some languages support it well (e.g., D, F#). Some languages support it poorly (e.g., C++, C#).
The big advantage to immutable value objects is that you can reason about them well. Their methods (member functions) are not going to mutate their state, because the object is immutable. Immutable value objects can be shared and can be shared across threads. And the things sharing them won't be surprised.
For object-oriented programming, immutable value objects make sense. But immutable identity objects does not make much sense.
An identity object is something that holds state that changes over time. So to make one immutable is in contention with what an identity object represents: transitioning from one valid state to another valid state, with that state change being encapsulated and able to guarantee the object is always coherent. Assuming no bugs.
(There's also service objects, which provide services... but they are either stateless, or their state is all internal and hidden, so they seem stateless. They often follow the singleton pattern.)
Because the language so strongly impacts immutability as an idiom, languages which support immutability as part of the core language will in turn be utilized by the programmers.
Languages that support it poorly will be used ad hoc (if at all). The other developers on the team may find the usage to be atypical, and during maintenance may violate the what-should-be-immutable object's immutability... unintentionally.
For example, I use C++, consider
std::shared_ptr<Foo const>
is a shared immutable Foo object. If I saw someone on my team using them, my first reaction would be "what the heck...?"But in F#, the opposite is the case. Everything is immutable unless declared mutable, and when I see something declared mutable in F# my first reaction would be "what the heck...?"
(F# does support object-oriented programming, because it is a .NET language. But because its roots is OCaml and functional-first functional programming, immutability is strongly reflected in idiomatic F# programming.)
The first programming language I learned was JavaScript, but the second was Scheme, a functional language. Mutating values is possible, but instructors made it clear that unless you had good reason, they strongly encouraged us to avoid it.
The contrast was astonishing. Scheme was so much easier to debug. I found that because the variables were immutable, it was really easy to find where things went wrong, or trace where values came from.
The answer to "What code changed this? And why?" could take you anywhere in the codebase. You can drown in a vast universe of cascading side-effects.
The answer to "What function returned this value?" takes about ten seconds.
I mostly work in Java and JavaScript, and the hardest bugs I've ever seen (and after 18 years doing this, I've seen some nasties) were all because of code that relied on side-effects.
Side-effects are necessary for lots of things. But it's best to avoid them when possible. Your code will be easier to debug and maintain.
The best way I know to really get an understanding of the benefits of immutability is to write and debug some code written with immutable data structures.
I highly recommend checking out The Little Schemer, which is a deceptively accessible introduction to functional programming, data structures, etc. I used to work through it once or twice a year to stay sharp, and I really should get back in the habit.
There are a few wonderful Swift talks around this topic to explain why value types are in the language:
e.g. developer.apple.com/videos/play/ww...