用 Elixir 制作带有封口的计数器

Making a counter with a closure in Elixir

提问人:eje211 提问时间:11/15/2023 更新时间:11/16/2023 访问量:81

问:

我正在学习 Elixir,我刚刚谈到了关于闭合的部分。当一门语言有闭包时,我通常做的第一件事就是尝试制作闭包算法。在 JavaScript 中,它看起来像这样:

let counter = function() {
    let count = 0;
    return function() {
        count += 1;
        return count;
    };
}();

然后,每次调用时,它将按顺序返回一个新号码。counter

counter(); //  returns 1
counter(); //  returns 2
counter(); //  returns 3
counter(); //  etc.

有没有可能在长生不老药中做到这一点?主要问题似乎是在Elixir中是不可变的。我可以把它做成一个单元素列表,但这听起来像是一个坏主意™。处理这种纯粹假设情况的灵丹妙药方法是什么?count

JavaScript Elixir 闭包不 可变性

评论


答:

0赞 7stud 11/15/2023 #1

主要问题似乎是在Elixir中是不可变的。count

iex(1)> count = 1
1

iex(2)> IO.puts count
1
:ok

iex(3)> count = 2
2

iex(4)> IO.puts count
2
:ok

在 elixir 中,值是不可变的,但我可以让变量指向内存中存储其他值的不同位置。例如,当该行执行时,2 存储在内存中的某个位置,然后绑定到该新内存位置。之后,没有变量绑定到内存位置 1,以便内存已准备好进行垃圾回收。count = 2count

Elixir 具有闭包,因为函数在定义函数的环境中确实带有绑定,但绑定是指向特定内存位置的,并且这些内存位置的值是不可变的:

defmodule A do
  def counter do
    count = 0 

    fn -> 
      count = count + 1
      count
    end

  end

end

在 iex 中:

iex(6)> c("a.ex")          
[A]

iex(7)> counter = A.counter
#Function<0.55300721/0 in A.counter/0>

iex(8)> counter.()         
1

iex(9)> counter.()
1

iex(10)> counter.()
1

我可以把它做成一个单元素列表,但这听起来像是一个坏主意™。

...这是行不通的。列表是内存中不可变值的优势的一个例子。当您向列表的头部添加一个值时,Elixir 会在内存中的其他地方创建一个全新的列表。但是,Elixir 知道旧列表是不可变的,而不是将旧列表复制到新列表的内存位置,因此 elixir 可以只使用指向旧列表的指针。新的内存位置由列表的新元素和指向旧列表的指针组成,无需复制。在闭包的情况下,绑定将绑定到内存中原始列表的位置,对原始列表的任何更改都将存储在内存中的其他位置。

处理这种纯粹假设情况的灵丹妙药方法是什么?

在 elixir/erlang 中,您可以使用称为 GenServer 的东西来保留函数调用之间的状态:

defmodule Counter do
  use GenServer

  #Client interface:

  def start_counter(starting_count) do
     GenServer.start_link(__MODULE__, starting_count)
  end

  def get_count(pid) do
    GenServer.call(pid, :increment)
  end
  

  # GenServer callback functions:

  @impl true
  def init(starting_count) do
    {:ok, starting_count}
  end

  @impl true
  def handle_call(:increment, _from, current_count) do
    {:reply, current_count, current_count+1} 
  end

end

当你写:

GenServer.call(pid, :increment)

Elixir 会寻找一个名为其第一个参数匹配的回调函数并执行它,并将状态作为第三个参数传入。您可以定义执行所需的操作,然后将回复发送回调用进程并设置新状态。handle_call():incrementhandle_call()

在 iex 中:

iex(1)> c("a.ex")                            
[Counter]

iex(2)> {:ok, pid} = Counter.start_counter(1)
{:ok, #PID<0.119.0>}

iex(3)> Counter.get_count(pid)               
1

iex(4)> Counter.get_count(pid)
2

iex(5)> Counter.get_count(pid)
3

iex(6)> Counter.get_count(pid)
4

评论

0赞 eje211 11/15/2023
我只是又想了一个小时,却一无所获。我得到的是 Elixir 很棒,但不是解决该特定问题的最佳语言。
0赞 7stud 11/15/2023
@eje211,我在关于闭包的答案中添加了一些东西,我不知道你是否有机会阅读它。 是长生不老药/Elrang的基础。它们被专门编程为在不可变的编程范式中处理需要保留状态的情况,例如计数器。GenServer 背后的编码很复杂,但 elrang 专家为我们做了所有的工作,发现了所有的错误,并给了我们一个经过实战考验的工具,现在我们可以非常简单地使用他们的劳动成果。GenServers
0赞 eje211 11/15/2023
在 JavaScript 示例中,内部变量是完全安全的(除非我遗漏了某些内容)。但是,在实践中,人们不会使用闭包。他们将使用一个 JavaScript 对象,该对象将受到各种竞争条件的约束。因此,虽然 JavaScript 示例更简单且同样安全(我认为),但这仅在非常不安全的上下文中才是正确的。也许这就是为什么更复杂的 Elixir 解决方案有意义的原因。我喜欢 Scala,但它非常沉重。长生不老药非常轻巧。我还不能理解它,但我认为我开始理解它的本质。
0赞 eje211 11/15/2023
我又读了一遍(我今天读了很多 Elixir 文档),是的,它现在非常有意义。我们从未真正看到过状态。我们只是在回调中得到它,然后在函数调用中发送新状态。这样一来,没有什么是可变的。非常聪明。
3赞 Adam Millerchip 11/15/2023 #2

如果需要状态,请使用进程。在Elixir中,惯用的方法是使用AgentGenServer

代理文档中的示例完全按照您的要求执行:

defmodule Counter do
  use Agent

  def increment do
    Agent.update(__MODULE__, &(&1 + 1))
  end

  # ...
end

用法:

Counter.start_link(0)
#=> {:ok, #PID<0.123.0>}

Counter.value()
#=> 0

Counter.increment()
#=> :ok

Counter.increment()
#=> :ok

Counter.value()
#=> 2