Immutability in this context is not about “you can’t change the data” at author/runtime time, but, rather in the underlying code that you cannot modifying an existing data structure in place. An example:
# Mutable data language
list = [a, b, c]
new_list = list.append(d)
inspect(new_list)
> [a, b, c, d]
inspect(list)
> [a, b, c, d]
The existing list has been modified and now has a new item. Hence all existing references will see the new contents.
# Immutable data language
list = [a, b, c]
new_list = list.append(d)
inspect(new_list)
> [a, b, c, d]
inspect(list)
> [a, b, c]
In this case the existing list has not been mutatated, the append operation creates a new list (and the underlying data structure, usually some kind of tree, keeps this performant), and existing references to the list will continue to see the unmodified list.
The obvious corrollary to this is becomes important how you manage references to data that can change. This often leads towards a “one big world map of state” pattern. I found it felt clumsy at first but quickly became very natural, especially in a language with good support for working with deeply nested data structures (Clojure has good support for this, for example, although I am sure there are others).
It’s especially well suited to event driven architectures because you can end up with something like:
world = {...}
receive do
{:event, evt} ->
world = handle_event(world, evt)
end
Now the only thing that can affect state is synchronous calls to handle_event
which keeps mutations in a box.