75
votes

In Dave Thomas's book Programming Elixir he states "Elixir enforces immutable data" and goes on to say:

In Elixir, once a variable references a list such as [1,2,3], you know it will always reference those same values (until you rebind the variable).

This sounds like "it won't ever change unless you change it" so I'm confused as to what the difference between mutability and rebinding is. An example highlighting the differences would be really helpful.

5
It's shadowing, not reassignment.rightfold

5 Answers

61
votes

Immutability means that data structures don't change. For example the function HashSet.new returns an empty set and as long as you hold on to the reference to that set it will never become non-empty. What you can do in Elixir though is to throw away a variable reference to something and rebind it to a new reference. For example:

s = HashSet.new
s = HashSet.put(s, :element)
s # => #HashSet<[:element]>

What cannot happen is the value under that reference changing without you explicitly rebinding it:

s = HashSet.new
ImpossibleModule.impossible_function(s)
s # => #HashSet<[:element]> will never be returned, instead you always get #HashSet<[]>

Contrast this with Ruby, where you can do something like the following:

s = Set.new
s.add(:element)
s # => #<Set: {:element}>
96
votes

Don't think of "variables" in Elixir as variables in imperative languages, "spaces for values". Rather look at them as "labels for values".

Maybe you would better understand it when you look at how variables ("labels") work in Erlang. Whenever you bind a "label" to a value, it remains bound to it forever (scope rules apply here of course).

In Erlang you cannot write this:

v = 1,      % value "1" is now "labelled" "v"
            % wherever you write "1", you can write "v" and vice versa
            % the "label" and its value are interchangeable

v = v+1,    % you can not change the label (rebind it)
v = v*10,   % you can not change the label (rebind it)

instead you must write this:

v1 = 1,       % value "1" is now labelled "v1"
v2 = v1+1,    % value "2" is now labelled "v2"
v3 = v2*10,   % value "20" is now labelled "v3"

As you can see this is very inconvenient, mainly for code refactoring. If you want to insert a new line after the first line, you would have to renumber all the v* or write something like "v1a = ..."

So in Elixir you can rebind variables (change the meaning of the "label"), mainly for your convenience:

v = 1       # value "1" is now labelled "v"
v = v+1     # label "v" is changed: now "2" is labelled "v"
v = v*10    # value "20" is now labelled "v"

Summary: In imperative languages, variables are like named suitcases: you have a suitcase named "v". At first you put sandwich in it. Than you put an apple in it (the sandwich is lost and perhaps eaten by the garbage collector). In Erlang and Elixir, the variable is not a place to put something in. It's just a name/label for a value. In Elixir you can change a meaning of the label. In Erlang you cannot. That's the reason why it doesn't make sense to "allocate memory for a variable" in either Erlang or Elixir, because variables do not occupy space. Values do. Now perhaps you see the difference clearly.

If you want to dig deeper:

1) Look at how "unbound" and "bound" variables work in Prolog. This is the source of this maybe slightly strange Erlang concept of "variables which do not vary".

2) Note that "=" in Erlang really is not an assignment operator, it's just a match operator! When matching an unbound variable with a value, you bind the variable to that value. Matching a bound variable is just like matching a value it's bound to. So this will yield a match error:

v = 1,
v = 2,   % in fact this is matching: 1 = 2

3) It's not the case in Elixir. So in Elixir there must be a special syntax to force matching:

v = 1
v = 2   # rebinding variable to 2
^v = 3  # matching: 2 = 3 -> error
39
votes

Erlang and obviously Elixir that is built on top of it, embraces immutability. They simply don’t allow values in a certain memory location to change. Never Until the variable gets garbage collected or is out of scope.

Variables aren't the immutable thing. The data they point to is the immutable thing. That's why changing a variable is referred to as rebinding.

You're point it at something else, not changing the thing it points to.

x = 1 followed by x = 2 doesn't change the data stored in computer memory where the 1 was to a 2. It puts a 2 in a new place and points x at it.

x is only accessible by one process at a time so this has no impact on concurrency and concurrency is the main place to even care if something is immutable anyway.

Rebinding doesn’t change the state of an object at all, the value is still in the same memory location, but it’s label (variable) now points to another memory location, so immutability is preserved. Rebinding is not available in Erlang, but while it is in Elixir this is not braking any constraint imposed by the Erlang VM, thanks to its implementation. The reasons behind this choice are well explained by Josè Valim in this gist .

Let's say you had a list

l = [1, 2, 3]

and you had another process that was taking lists and then performing "stuff" against them repeatedly and changing them during this process would be bad. You might send that list like

send(worker, {:dostuff, l})

Now, your next bit of code might want to update l with more values for further work that's unrelated to what that other process is doing.

l = l ++ [4, 5, 6]

Oh no, now that first process is going to have undefined behavior because you changed the list right? Wrong.

That original list remains unchanged. What you really did was make a new list based on the old one and rebind l to that new list.

The separate process never has access to l. The data l originally pointed at is unchanged and the other process (presumably, unless it ignored it) has its own separate reference to that original list.

What matters is you can't share data across processes and then change it while another process is looking at it. In a language like Java where you have some mutable types (all primitive types plus references themselves) it would be possible to share a structure/object that contained say an int and change that int from one thread while another was reading it.

In fact, it's possible to change a large integer type in java partially while it's read by another thread. Or at least, it used to be, not sure if they clamped that aspect of things down with the 64 bit transition. Anyway, point is, you can pull the rug out from under other processes/threads by changing data in a place that both are looking at simultaneously.

That's not possible in Erlang and by extension Elixir. That's what immutability means here.

To be a bit more specific, in Erlang (the original language for the VM Elixir runs on) everything was single-assignment immutable variables and Elixir is hiding a pattern Erlang programmers developed to work around this.

In Erlang, if a=3 then that was what a was going to be its value for the duration of that variable's existence until it dropped out of scope and was garbage collected.

This was useful at times (nothing changes after assignment or pattern match so it is easy to reason about what a function is doing) but also a bit cumbersome if you were doing multiple things to a variable or collection over the course executing a function.

Code would often look like this:

A=input, 
A1=do_something(A), 
A2=do_something_else(A1), 
A3=more_of_the_same(A2)

This was a bit clunky and made refactoring more difficult than it needed to be. Elixir is doing this behind the scenes, but hiding it from the programmer via macros and code transforms performed by the compiler.

Great discussion here

immutability-in-elixir

3
votes

The variables really are immutable in sense, every new rebinding (assignment) is only visible to access that come after that. All previous access, still refer to old value(s) at the time of their call.

foo = 1
call_1 = fn -> IO.puts(foo) end

foo = 2
call_2 = fn -> IO.puts(foo) end

foo = 3
foo = foo + 1    
call_3 = fn -> IO.puts(foo) end

call_1.() #prints 1
call_2.() #prints 2
call_3.() #prints 4
1
votes

To make it a very simple

variables in elixir are not like container where you keep adding and removing or modifying items from the container.

Instead they are like Labels attached to a container, when you reassign a variable is as simple a you pick a label from one container and place it on a new container with expected data in it.