I'm new to Elixir and functional programming coming from OO background.
I'm stuck trying to understand how variable reassignment works in Elixir in list comprehension. I expected functions test1() and test2() to print 4, but test2() doesn't reassign variable and prints 1.
defmodule SampleCode do
def test1 do
num = 1
num = num + 1
num = num + 2
IO.puts num # Prints 4
end
def test2 do
num = 1
for i <- (1..2) do
num = num + i
end
IO.puts num # Prints 1
end
end
Is this a variable scoping thing in Elixir or a basic principle in functional programming that I'm missing?
It is both actually.
Elixir allows rebinding only in the same scope and all constructs, with the exception of case
, cond
and receive
, introduce a new scope. Some examples:
num = 1
try do
num = 2
after
num = 3
end
num #=> 1
Funs:
num = 1
(fn -> num = 2 end).()
num #=> 1
Now some examples of the exceptions to the rule:
num = 1
case true do
true -> num = 2
end
num #=> 2
num = 1
cond do
true -> num = 2
end
num #=> 2
Even though, the cases above are somewhat discouraged as it is preferable to return the value explicitly:
num = 1
case x do
true -> 2
false -> num
end
#=> will return 1 or 2
The example above makes explicit what are the values being returned from case. Why those are supported in Elixir, even though not recommended, is a long story that has its origin in Erlang and continued due to some of the (very few) imperative macros in Elixir like if
and unless
. It will likely change as we move towards Elixir 2.0.
The best way to perform what you want is via the functions in Enum
. Your particular example could be done with Enum.sum/1
but any other complex example can be implemented with Enum.reduce/3
(almost all functions in Enum are implemented in terms of reduce, which may be fold in other languages).
To understand what happened, take a look at what elixir does to your code to syntactically allow rebinding, when the language* does not support it:
BEFORE AFTER
defmodule SampleCode do | defmodule SampleCode do
def test1 do | def test1 do
num = 1 | num_1 = 1
num = num + 1 | num_2 = num_1 + 1
num = num + 2 | num_3 = num_2 + 2
IO.puts num # Prints 4 | IO.puts num_3
end | end
|
def test2 do | def test2
num = 1 | num_1 = 1
for i <- (1..2) do | for i <- (1..2) do
num = num + i | num_2 = num_1 + i
end | end
IO.puts num # Prints 1 | IO.puts num_1
end | end
end | end
*Elixir's foundation is Erlang- Elixir's compilation produces Erlang .beam files, to be executed by the Erlang Virtual Machine. Erlang does NOT allow variable rebinding, so Elixir swaps out your variable names during compilation.
If you want to up your FP game quite a bit, and give yourself a tremendous leg up in writing Elixir, I would recommend forcing yourself to learn the Erlang syntax, and write strictly Erlang solutions to your problems for a month or few before going back to Elixir.
In case you are curious what actually happened to your code, here's the actual Erlang generated by Elixir:
1: test1() ->
2: num@1 = 1,
3: num@2 = num@1 + 1,
4: num@3 = num@2 + 2,
5: 'Elixir.IO':puts(num@3).
6: test2() ->
7: num@1 = 1,
8: 'Elixir.Enum':reduce(#{'__struct__' => 'Elixir.Range',
9: first => 1, last => 2},
10: [], fun (i@1, _@1) -> num@2 = num@1 + i@1 end),
11: 'Elixir.IO':puts(num@1).
Especially note line 11- the only variable it references is num@1
, which in Erlang cannot be altered by lines 8-10- no matter what happens in there, the value bound to num@1
is still 1
, because that's how it is first bound on line 7.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With