Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reassigning a variable in list comprehension

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
  1. Why do they these functions behave differently?
  2. Is this a variable scoping thing in Elixir or a basic principle in functional programming that I'm missing?
like image 757
Abs Avatar asked Feb 10 '23 03:02

Abs


2 Answers

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).

like image 170
José Valim Avatar answered Feb 15 '23 23:02

José Valim


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.

like image 35
Chris Meyer Avatar answered Feb 15 '23 23:02

Chris Meyer