【问题标题】:Reassigning a variable in list comprehension在列表理解中重新分配变量
【发布时间】:2015-09-06 22:34:05
【问题描述】:

我是来自 OO 背景的 Elixir 和函数式编程新手。

我一直试图了解 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 中的变量作用域还是我缺少的函数式编程的基本原则?

【问题讨论】:

  • 我很确定它只是作用域(阅读num2 = num+i)-顺便说一句:num = num + 1 之类的东西在 FP 中是一种反模式,因为如果不重新绑定值,这将毫无意义
  • 如果num = num + 1 在 FP 中不受欢迎,那么实现这个的正确方法是什么?递归?有问题的代码是我尝试做的其他事情的一个非常简化的版本,所以它并不像看起来那么简单:)
  • 是的-在原始级别递归-或稍后折叠之类的东西-在这种简化的情况下,您当然会说result = 1+1+2或简单地说result=4;)

标签: functional-programming list-comprehension elixir


【解决方案1】:

这是 Elixir 中的变量作用域还是我缺少的函数式编程的基本原则?

其实两者都是。

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

尽管如此,上述情况还是有些不鼓励,因为最好显式返回值:

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

上面的示例明确了从 case 返回的值是什么。为什么即使不推荐在 Elixir 中支持这些,也是一个很长的故事,它起源于 Erlang,并且由于 Elixir 中的一些(非常少的)命令式宏,如 ifunless 而继续存在。随着我们向 Elixir 2.0 迈进,它可能会发生变化。

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

【讨论】:

    【解决方案2】:

    要了解发生了什么,请查看当语言* 不支持时,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 行第一次绑定的方式。

    【讨论】:

    • 请注意:与其说 Erlang 不支持多重赋值,不如说是 Erlang 在哪里强加了一个新的范围。 Erlang 中的区别也很明显:当您在新范围内时,将无法从外部访问新变量。并且显示 Elixir 编译成的 sn-p 是整洁的。 :)
    • @JoséValim:再看一遍,我的反汇编有点不对劲,因为 Erlang 变量是小写的。我不能把它归功于你-原始来源是其他人,我很确定他们是从示例中得到的 here
    猜你喜欢
    • 2015-11-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-07-20
    相关资源
    最近更新 更多