In my experience with Erlang I have found the times I want to actually re-bind a variable is much more common than when I actually want to match it.
I also enjoy the ^var
approach in Elixir because it makes
very explicit that I want a match to happen. It is explicit
the variable was previously defined.
Furthermore, macros increases the chances of variables matching which would be a disaster if we didn't allow re-binding.
Consider the example where you have a data and you want it to pass through a bunch of transformations in Erlang:
f(X) ->
X1 = foo(X),
X2 = bar(X1),
baz(X2).
Now, imagine that you want to add something in between, it requires you to change everything that comes after the transformations in between:
f(X) ->
X1 = foo(X),
X2 = fab(X1),
X3 = bar(X2),
baz(X3).
One solution to this problem is to nest the calls:
f(X) ->
baz(bar(fab(foo(X)))).
But I find this approach very unreadable as you need to read your code inside out (from the most inner call).
Elixir solves this in two ways. Re-binding is one of them
and the |>
operator is the other:
# Rebinding...
def f(x) do
x = foo(x)
x = fab(x)
x = bar(x)
baz(x)
end
# Pipe...
def f(x) do
x |> foo() |> fab() |> bar() |> baz()
end
Notice how |>
avoids the re-binding all together
but make it visible the series of transformation the
data goes thourgh.
Another issue that happens in Erlang is the accidental match when you try to assign a variable that already exists by mistake. Those are not frequent in Erlang but a lot more in Elixir. In order to see an example, let's talk about how Elixir compiles to Erlang AST.
A couple versions ago, a variable was represented in Elixir AST (aka quoted expression) as:
{ :var_name, 1, nil }
Where 1
represents the line number the variable was
defined. This translates to Erlang AST as:
{ :var, 1, :var_name }
At some point, we decided that it would be best if the second element of Elixir's tuple was a container for metadata and not only line numbers. So now we have:
{ :var_name, [line: 1], nil }
The code that translates this to Erlang AST would be like that:
translate({ VarName, MetaData, nil }) ->
{ var, ?line(MetaData), VarName }.
Notice we created a macro ?line(MetaData)
to extract
the line number from the MetaData
simply because we need
this convenience throughout almost all of Elixir's compiler
code. This macro was first implemented as follows:
-define(Line,
case lists:keyfind(line, 1, Opts) of
{ line, Line } when is_integer(Line) -> Line;
false -> 0
end).
There is a huge issue with this macro. I can only use it once
per function clause! If, it happens that I need to use it twice,
the Line
variable would be defined the first time and a match
attempt would happen the second time I call ?line
.
That's how I found out that Erlang macros (more like templates) are not hygienic. I have since then moved the implementation to a module that the macro calls directly.
Now, imagine this problem in Elixir: macros are very common to use! Even though we have macros hygiene in place, not supporting re- binding would trigger many accidental matches. Approaches like gensym could help solve the problem, but putting the burden in the developer (and I am not sure how efficiently they could be implemented in the Erlang VM). I think most LISP allow rebinding on LET (and in general you use less variables in LISPs) (citation required).
Notice the current aproach employed by Elixir's compiler does not affect the immutability guarantees (as you can't change the binding of a function).
Sorry for the long answer. :)
Since this may be a concern, one more clarification: while Elixir macros allow a macro to inject a variable into the user code, this behaviour is not encouraged and hygiene ensures by default a variable used in the macro is not going to conflict with a user variable.