Elixir Variable Rebinding

Matt Furness
everydayhero engineering
4 min readDec 19, 2016

Here at everydayhero we are starting to use more and more Elixir in production systems, so as more of the team start to learn the language and it’s frameworks, we thought it might be nice to share some of the interesting tidbits that we encounter along the way. The tidbit we would like to share with you in this post is around variable rebinding and how it behaves in various scopes.

Immutability and rebinding

Elixir is an immutable language (it has to be to run on the Erlang VM), so it comes as a surprise to many, myself included, that the following does not produce an error.

iex> animal = "dog"
"dog"
iex> animal = "cat"
"cat"

The first piece of the puzzle to understand is that the = operator is called the match operator, it is not called the assignment operator. So Elixir can make the match animal = "dog" true by binding the animal variable to "dog". Depending on other languages you are familiar with you might have expected the animal = "cat" expression above to fail with a message about re-assigning an immutable variable, or a message that "dog" does not match "cat". What happens in Elixir is that the variable is rebound to "cat" when Elixir is making the match animal = "cat" true.

So what is happening under the hood if Elixir is an immutable language and variables are rebound? In the example above Elixir is giving you an entirely new animal variable that is pointing to the "cat" value. The old "dog" value is no longer accessible and it will get cleaned up during garbage collection.

There is an excellent article that goes into why the decision to rebind, instead fail due to match errors, was made. The one point that particularly struck a chord with me is:

Erlang requires both previous and further knowledge of the context when introducing new variables while Elixir requires only further knowledge.

= really is the match operator

Just to re-iterate the point that the = is the match operator, let’s look at the following expression.

iex> [dog, dog] = ["dog", "cat"]** (MatchError) no match of right hand side value: ["dog", "cat"]

Elixir errors if, when attempting to make a match true, the same variable would be bound to more than one value. In the example above, to satisfy the match, the variable dog would represent both the value "dog" and the value "cat" which does not make sense, and so the expression produces an error.

What if we want to match?

So now that we know Elixir rebinds variables, how do we match against the bound value of a variable instead of rebinding that variable. One option is to simply reverse the expression.

iex> animal = "dog"
"dog"
iex> "cat" = animal
** (MatchError) no match of right hand side value: "dog"

The reason this works is because Elixir will only bind variables on the left hand side of the match operator. The second, and more idiomatic approach, is to use the pin operator (^). The pin operator makes your intent nice and explicit.

iex> animal = "dog" 
"dog"
iex> ^animal = "cat"
** (MatchError) no match of right hand side value: "cat"

The scope of rebinding

When a variable is rebound it will only dereference to the rebound value in the scope that the rebinding took place. Take the following expressions:

iex> animal = "dog"
"dog"
iex> cat_rebinder = fn ->
...> animal = "cat"
...> end
#Function<20.52032458/0 in :erl_eval.expr/5>
iex> cat_rebinder.()
"cat"
iex> animal
"dog"

In the above example cat_rebinder is bound to an anonymous function, that when called rebinds the animal variable to "cat". However, the animal variable is only bound to the new value "cat" in the scope of the anonymous function. That is why, even after invoking the cat_rebinder function, the subsequent dereferencing of the animal variable is still the original "dog" value.

When a function closes over a variable it remains bound to the value at that time in the functions scope. Let’s have a look at another set of expressions:

iex> animal = "dog"
"dog"
iex> animal_printer = fn ->
...> IO.puts animal
...> end
#Function<20.52032458/0 in :erl_eval.expr/5>
iex> animal = "cat"
"cat"
iex> animal_printer.()
dog

In the above example the animal_printer anonymous function closes over the animal variable. The animal variable will remain bound to the current value at the time the anonymous function is bound to the variable animal_printer, in this case "dog". That is why, even after animal is rebound to "cat" in the outer scope, invoking animal_printer prints dog.

One final note that might not be immediately obvious is that named functions are not closures. Every time you declare a function with def you get a new variable scope. Let’s take a look at a final set of expressions:

iex> defmodule Animals do 
...> animal = “dog”
...>
...> def animal_printer do
...> IO.puts animal
...> end
...> end
warning: variable animal is unused
iex:2
** (CompileError) iex:4: undefined function animal/0
(stdlib) lists.erl:1338: :lists.foreach/2
(stdlib) erl_eval.erl:670: :erl_eval.do_apply/6
(iex) lib/iex/evaluator.ex:135: IEx.Evaluator.handle_eval/6

The module expression will not even compile because the animal variable simply does not exist in the scope of the animal_printer function.

Wrapping up

Hopefully this has shed some light on some of the subtleties of binding variables, and how that behaves in various scope, in Elixir.

--

--