Rebinding Variables in Elixir

by Jeremy D. Frens on May 8, 2016
part of the Ruby, Elixir, Variable Bindings, and Concurrency series


Elixir does not allow side effects. It can’t allow them. Elixir is based on Erlang and runs on the BEAM (Erlang’s virtual machine), and they treat side effects as errors. And yet, it appears as if Elixir does allow variable reassignment. We’ll explore what’s going on in this article.

I posted an article last week about side effects in Ruby to set your expectations. We walked through some examples of how Ruby handles variable scope and side effects in closures. Read that article first for some context. Or just jump straight into this article to have your mind blown!

Elixir does not allow side effects!

Let’s try reassigning a value to a variable in Elixir. If the section title is to be believed, reassignment is not possible because Elixir does not allow side effects:

x = 5 # => 5
x # => 5
x = 10 # => 10
x # => 10

Oh… um… okay? That’s a reassignment on the third line. Apparently, I cannot be believed. At least this is a really short blog article, right?

Or maybe things aren’t quite what they seem.

Aside: pinned variables

As I mentioned in the introduction, Erlang does not allow side effects; we’ll get an error when we try to assign a new value to a variable. We can trigger this same error by pinning the variable in Elixir:

x = 5
^x = 10 # ** (MatchError) no match of right hand side value: 10

Technically, = is the match operator, not an assignment operator. It tries to find a match for the pattern on the left with the computation on the right. When we pin variables on the left, the match operator is not allowed to change its value. x is pinned and bound to 5, and 5 does not match 10.

If we match ^x to 5, Elixir is happy:

^x = 5 # => 5
x # => 5

With a simple pattern of just one variable, this behavior might seem a little odd. It’s more understandable when used in a larger pattern:

x = 5
{y, ^x} = {10, 5}
y # => 10
x # => 5

This fails if x does not match:

x = 5
{z, ^x} = {8, 99} # ** (MatchError) no match of right hand side value: {8, 99}

Erlang and Elixir subscribe to the fail-fast school, and pattern matches like this with a pinned variable or a literal value are used frequently to trigger a failure.

Just like Ruby

Consider these functions:

h = fn x -> x + 10 end
g = fn x -> x = x + 10 ; x end

These are the Elixir equivalents of the Ruby functions in the previous article. They behave the same in Elixir as in Ruby because we’re not playing any games with closures and scopes:

x = 5
h.(9) # => 19
x # => 5

x = 100
h.(20) # => 30
x = -3
h.(20) # => 30

g is equally well behaved.

Closures

x = 5
k = fn -> x * 2 end
f = fn -> x = x * 2 ; x end

Let’s just concentrate on f since it does everything that k does plus a side effect:

f.() # => 10

Same as in Ruby.

However, in Ruby, the x at the top level was also reassigned the value 10; not so much in Elixir:

x # => 5

Whoa.

Call f as many times as you like; x remains set to 5.

At the beginning of this article, it seemed like Elixir allowed us to reassign a new value to x; but this result says different. Obviously f is reading x to compute x*2, but why doesn’t the result get reassigned back to x?

The strangeness doesn’t end there.

In Ruby, when we assigned a new value to x at the top level, f computed a different value. Not so in Elixir:

x = 256
f.() # => 10

Reassign whatever value you like to x, and f will keep returning 10.

Mind blown.

Call f as many times as you like, it will always return 10 and not change x. Change x all you want, and f will still evaluate to 10.

Reassignment is not what you think it is

f demonstrates that what I’ve been calling “reassignment” in Elixir is not actually a reassignment. From the first example changing the top-level x it appeared like we had reassignments, but Elixir is hiding something from us.

When you reassign a value to a variable in Ruby, you are affecting a memory location. Within one scope, the variable always refers to the same memory location. When you shadow the variable, you have a new memory location for that variable in that scope; when control leaves that scope, you’re back to the original memory location.

In Elixir, it’s maybe more accurate to say that a variable is just a name (not a memory location); I can bind a value to a variable; I can reference a variable to gets its value; and I can match on its value (by pinning it). But when I rebind a name to a different value, that binding has its own scope, its own memory location.

Here’s the whole sequence again:

x = 5
x # => 5
f = fn -> x = x * 2 ; x end
f.() # => 10
x # => 5

When f references x in x*2, it’s referring to the x defined at the top level on the first line. The result of x*2 is put into a completely different x, valid only for the scope of that function. Since this new x shadows the one at the top level, it’s value is returned.

Similarly, f captures one particular x, one particular memory location:

x = 256
x # => 256
f.() # => 10

= always rebinds (not reassigns) unpinned variables. You’re not changing the value in x; you’re creating a new x. The x in x = 256 is not any x in f or the previous x at the top level. It’s a brand new x. f still refers to the x that was in scope when f was defined, and that x is now (and will remain) 5.

What happens if we redefine x and f?

x = 256
f = fn -> x = x * 2 ; x end

The freshest x at the top level is bound to 256, and that version is in scope for this definition of f, so this f will return 512.

f.() # => 512

Alpha conversion makes things clearer

This is pretty confusing when we keep reusing f and x. In the previous article we used alpha conversions to give each unique variable its own name. In fact, after an alpha conversion, it is safe to say that each unique name is a separate memory location.

Alpha conversion will help us here, too. We just need to rename the variable whenever it is rebound, so that

x = 5
x = x * 2
x = x * 3

becomes

x = 5
y = x * 2
z = y * 3

Now the names reflect when and how variables as memory locations are used. Let’s rename the variables in our x-and-f example:

x = 5
f = fn -> a = x * 2 ; a end
f.() # => 10
x # => 5

y = 256
f.() # => 10
y # => 256

ff = fn -> b = y * 2 ; b end
ff.() # => 512
y # => 256

Now you can use your intuitions about lexical scoping to make sense of what’s going on: x doesn’t change because it’s never reassigned a new value; f always refers to the original x because that’s the only x now.

Elixir does not allow side effects. It uses scopes to hide them.

Isn’t this terribly confusing?

Hopefully the alpha-converted version makes things clearer (although the contrived nature of the example doesn’t help). You can always use alpha conversion to make better sense of your code if you think this rebinding is a problem.

But I don’t think it’s normally a problem. I had to deliberately cook up my examples for this blog article; they didn’t arise naturally out of my own Elixir code.

However, knowing that this rebinding could be a problem, you might be a bit skittish about your variable names. The solution is not to give each variable a unique name. Don’t. If anything, I’d suggest that you reuse names all the time and get it really, really wrong. Pin a couple variables and see what happens. See what errors you get; debug the errors; learn from the errors. That’s how you get a handle on these sorts of things.

The more practical solution is to keep your functions short. The shorter they are, the less likely you’ll reuse a variable name. Or it will be reused in a different function, in an obviously different scope.

But Ruby makes so much sense!

It does? Really?

Keep in mind that h and g work exactly the same in Ruby and Elixir. If you’re not doing side effects, the languages compute the same, so Elixir is as good as Ruby.

The problem in Ruby is when you have reassignments and captured scopes. Read the previous article again. Skip to the section where f and x screw each other over whenever you call f or change x. Do you really want to compute with f? Keep in mind that “I won’t write something like f” is really an admission that you’d rather be coding in Elixir where you can’t write something like f.

We’ll look at other ways Ruby fails us in the next article.

Aside: why not do what Erlang does?

Elixir uses this “new name” rebinding to eliminate side effects while Erlang just bans them outright, killing the process if you try to reassign a variable.

Why the difference? José Valim addressed that in a blog post: “Comparing Elixir and Erlang variables”. I won’t bother even summarizing the article because, as always, José does a great job, and it is well worth a full read.

elixir