在列表理解中重新分配变量

Reassigning a variable in list comprehension

我是 Elixir 和函数式编程的新手,具有 OO 背景。

我一直在尝试了解 Elixir 中变量重新分配在列表理解中的工作原理。我希望函数 test1() 和 test2() 打印 4,但 test2() 不会重新分配变量并打印 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. 为什么这些函数的行为不同?
  2. 这是 Elixir 中的变量作用域还是我遗漏的函数式编程的基本原则?

Is this a variable scoping thing in Elixir or a basic principle in functional programming that I'm missing?

其实两者都是

Elixir 仅允许在相同范围和所有构造中重新绑定,casecondreceive 除外,引入新范围。一些例子:

num = 1

try do
  num = 2
after
  num = 3
end

num #=> 1

乐趣:

num = 1
(fn -> num = 2 end).()
num #=> 1

现在是规则例外的一些示例:

num = 1
case true do
  true -> num = 2
end
num #=> 2

num = 1
cond do
  true -> num = 2
end
num #=> 2

尽管如此,上述情况还是有些令人沮丧,因为它比 return 明确的值更可取:

num = 1
case x do
  true  -> 2
  false -> num
end
#=> will return 1 or 2

上面的示例明确说明了从大小写 return 编辑的值是什么。为什么这些在 Elixir 中得到支持,尽管不被推荐,这是一个很长的故事,它起源于 Erlang,并且由于 Elixir 中的一些(非常少的)命令式宏而继续存在,比如 ifunless。随着我们转向 Elixir 2.0,它可能会发生变化。

执行所需操作的最佳方式是通过 Enum 中的函数。您的特定示例可以用 Enum.sum/1 完成,但任何其他复杂示例都可以用 Enum.reduce/3 实现(Enum 中的几乎所有函数都是根据 reduce 实现的,这可能在其他语言中被折叠)。

要了解发生了什么,请看一下 elixir 对您的代码做了什么以在语法上允许重新绑定,当语言*不支持它时:

         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 的基础是 Erlang——Elixir 的编译生成 Erlang .beam 文件,由 Erlang 虚拟机执行。 Erlang 不允许变量重新绑定,所以 Elixir 在编译期间换掉你的变量名

如果你想提高你的 FP 游戏水平,并在编写 Elixir 时给自己一个巨大的优势,我建议你强迫自己学习 Erlang 语法,并用一个月的时间严格编写 Erlang 解决方案来解决你的问题或很少再回到 Elixir。

如果您想知道您的代码实际发生了什么,这里是 Elixir 生成的实际 Erlang:

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

特别注意第 11 行 - 它引用的唯一变量是 num@1,在 Erlang 中,第 8-10 行不能更改它 - 无论那里发生什么,值绑定到 num@1仍然是 1,因为这是第 7 行第一次绑定的方式。