Immutability explanation

a = 1
a = 2

How can the first assignment a = 1 be immutable after the a = 2 rebind?

1 Like

The value of 1 hasn’t changed, though it’s not accessible under the name a anymore, as there is a new thing named a with the value 2 “shadowing” the old a.

2 Likes

You can try this code:

a = 1
f = fn -> a end
a = 2
IO.puts("a: #{a}")
IO.puts("f(): #{f.()}")
a: 2
f(): 1

But as often, it has nothing to do with immutability, it is just how scoping rules work in elixir.

2 Likes

So the first 1 is still in memory?

if you run this code only:

a = 1
a = 2

The 1 would be on the stack memory segment, so it would not exist anymore. But I think the compiler will just skip that value completely.

1 Like

How can the first assignment a = 1 be immutable after the a = 2 rebind?

First you need to know that the = operator is called the match operator, it is not called the assignment operator.

So, Elixir can make the match a = 1 true by binding the a variable to the specified value.

Hence, what happens next in Elixir is that the variable is rebound to the new value when Elixir is making the match a = 2 true.

Take into account that Elixir will only bind variables on the left hand side of the match operator.

5 Likes

Yes, from the beams point of view, the first a is actually named Va_1, the second one is Va_2, though from elixir you can only ever “see” the last binding from the current scope.

4 Likes

Any way to access previous binding?

As I said, no.

1 Like

As I always say, variables are mutable in Elixir, no matter what fancy explanations are given to me :wink:

In my point of view, whats matter, its what I can see from outside the box, not what’s going inside the box.

Outside the box, the developer will not have the guarantee that a variable keeps the value it was first assigned, thus for me they are mutable.

Inside the box the Beam just points the variable to other location in memory where the new value its stored, or something similar, and normally is referred as rebind, thus its why everyone says that they are immutable.

In the end of the day what matters for me as a developer is the public interface the language presents to me, and I don’t care how its done under the hood(inside the box), I just care how it looks without the need to look inside the box.

NOTE: And yes I already have read Comparing Elixir and Erlang variables « Plataformatec Blog

You do, though, absolutely have a guarantee that nothing inside the box can change a variable that you have assigned.

2 Likes

Elixir variables are not mutable. They are reattachable. You can think of Erlang/Elixir variables as stickers that can be attached to data. The difference between an Erlang variable and an Elixir variable (in the eyes of a plain Erlang/Elixir user like myself) is that in Elixir, you can tear off the sticker and attach it to another data structure.

3 Likes

When you pass a variable to a function, you have the guarantee that it will not/cannot be changed; therefore we can’t say that variables are behaving like mutable variables in Elixir.

Let’s take a step back and rethink of the challenges to work with variables/state that are mutable:

By passing a mutable data structure (such as an object in oop, an array, etc.) to a function or object’s method, you don’t know if your data structure is susceptible to be changed by that function. The only way to know is by analyzing the code of the function being called (and maybe that function passes the data around to other functions). This makes your code much harder to reason about, especially code made by less experienced programmers who mutate structures without thinking about consequences.
In Elixir you do not have that problem; everything is immutable by nature. You don’t ever need to explicitly make a copy of your data. Most of your functions that deal with application logic in Elixir are pure, while in oop/imperative languages most of them are impure, because oop languages actually encourage coding with mutability.

A variable in Elixir can indeed be reassigned, so if you have a huge code block (and you should really never have that or rarely), and you want to know the value of some variable, you can’t be sure if it has been reassigned at some point or not. But then again, this problem should happen very rarely, most functions should not have a big amount of lines of code; for me the most important is as said above, that data that is being passed around cannot be changed; that is huge as I believe that most bugs in software are bugs due to invalid state.

3 Likes

The variable refers to what the programmer sees; variables in elixir are mutable. Like “variable” means the symbol that gets assigned (e.g. “solve for x”. X is the variable). When you solve for x and find that it’s 5, that 5 is immutable, it was always there, and you call that the value. Likewise, in Elixir, it’s the value that is immutable. When you are rebinding the programmer-visible symbol in elixir, you are mutating it’s meaning; you are mutating the variable. The value, of course, is not mutated, unless you’re using a nif 3:-)

Just saying “variables are mutable” is also misleading in Elixir’s case. As it leads people into thinking that you can mutate a variable inside a control structure like if and have that change visible after the structure. Or like in the case of closures:

a = 15
f = fn -> a = 24 end
f.()
IO.inspect(a)

Personally I say “data is immutable, variables can be rebound”.

3 Likes

This is all about the terminology, but we seem to confuse reassignment/rebinding and mutation.

a relatively permanent change in hereditary material that involves either a change in chromosome structure or number […]
— Mutation Definition & Meaning - Merriam-Webster

Consider ruby.

a = {foo: 42}
b = a 
a = :bar

The above does not mutate anything, b remains {foo: 42}. This is reassignment, not a mutation at all. Here is a mutation.

a = {foo: 42}
b = a 
b[:foo] = 3.14159265

Using the proper wording we can easily see that variables in Elixir are indeed immutable.

1 Like

You’re conflating the variable for the value. Reassigning, in your terms, changes, its heredity, if you will.

Julia does a great job of explaining this clearly:

https://docs.julialang.org/en/v1/manual/variables/

Yes, and I perfectly know that, and I love this behavior, but that doesn’t change anything in how the variable behavior is seen from outside the box, aka that it’s value have changed, and something that changes cannot be called immutable.

And we will continue to correct you. :slight_smile:

Since you are talking about guarantees, mutability and rebinding provide different guarantees.

With rebinding, you fully know what is the value of a variable at compile time. In other words, by simply reading the code, you know what the variable points to. For example, in Elixir:

a = []
some_function(a)
a #=> []

It doesn’t matter what some_function/1 does, the value of a stays the same: an empty list. Sure, you can rebind:

a = []
a = [1]
some_function(a)
a #=> [1]

But you can look at the compiled code and all of the places that change a are in front of you. This is often called a lexical property. Aliases, imports, and requires in Elixir are also lexical. This means you only need to look at the current file (or the current scope) to understand what will happen to the code. Rebinding is a lexical property.

With mutability, on the other hand, the value of variable can change in a separate scope, without any clue in the calling convention:

a = []
some_function(a)
a #=> [1]

In a mutable language, some_function could be defined in the same file or in a complete separate file and it would change the value of a. Therefore, when talking about guarantees, rebinding provide more guarantees than mutability, since with rebinding you don’t have to look at a separate function to figure out if a is going to change.

To sum up, in order to understand what will happen with a variable a in each language:

  • with static single assignment (as in Erlang) - you only need to look at the expression that defines the variable
  • with rebinding (as in Elixir) - you only need to look at the current scope
  • with mutability (as in Ruby) - you need to look at the current scope plus all functions called by the current scope and the usage of all closures returned by the current scope

Sure, you can go ahead and ignore this, but when you put rebinding and mutability in the same bucket, you are discarding important differences in guarantees provided by the models.

EDIT1: btw, nothing I said above refers to the implementation of the language. Those are all public properties of the language(s) - with no implementation details.

EDIT2: I removed the section about “lexically mutable” because saying “variable is mutable” or “variable is immutable” are both wrong. Those adjectives do not usually apply to variables. It would be like saying “apple is fast”. We do say however that values (or objects) are mutable or immutable. A variable that doesn’t change is usually called a constant in imperative languages or single assignment in FP terms. Changing a variable is typically reassignment or rebinding. Ruby is actually a good language to show all possible combinations:

array = []        # variable (with rebinding) and mutable array (value)
ARRAY = []        # constant (single assignment) and mutable array (value)
array = [].freeze # variable (with rebinding) and immutable array (value)
ARRAY = [].freeze # constant (single assignment) and immutable array (value)
20 Likes