编写函数"一次"在Elixir

时间:2017-11-23 09:41:02

标签: functional-programming elixir immutability memoization

我主要是从Javascript背景来到Elixir。在JS中,可以编写更高阶的函数"一次"返回一个只调用传入函数一次的函数,并在后续调用中返回前一个结果 - 该技巧操纵通过闭包捕获的变量:

var once = (func) => {
    var wasCalled = false, prevResult;
    return (...args) => {
        if (wasCalled) return prevResult;
        wasCalled = true;
        return prevResult = func(...args);
    }
}

在我看来,由于其不同的变量重新绑定行为,它无法在Elixir中创建此函数。有没有其他聪明的方法通过模式匹配或递归来做,或者它是不可能的?没有宏,我想象那些可能会启用宏。感谢

3 个答案:

答案 0 :(得分:3)

使用当前流程词典:

(@RoleId = 1 AND _t1.CreatedBy = @RoleId) OR ISNULL(@RoleId,0)<> 1

或者,如果函数将跨进程传递,则可以使用defmodule A do def once(f) do key = make_ref() fn -> case Process.get(key) do {^key, val} -> val nil -> val = f.() Process.put(key, {key, val}) val end end end end 表:

ets

# ... during application initialization :ets.new(:cache, [:set, :public, :named_table]) defmodule A do def once(f) do key = make_ref() fn -> case :ets.lookup(:cache, key) do [{^key, val}] -> val [] -> val = f.() :ets.insert(:cache, {key, val}) val end end end end / Application.put_env也可用于保存全局状态,但通常用于配置设置。

答案 1 :(得分:2)

在大多数情况下,它不被视为惯用语,但您可以使用Agent执行此操作:

defmodule A do
  def once(fun) do
    {:ok, agent} = Agent.start_link(fn -> nil end)
    fn args ->
      case Agent.get(agent, & &1) do
        nil ->
          result = apply(fun, args)
          :ok = Agent.update(agent, fn _ -> {:ok, result} end)
          result
        {:ok, result} ->
          result
      end
    end
  end
end

现在如果你运行这个:

once = A.once(fn sleep ->
  :timer.sleep(sleep)
  1 + 1
end)

IO.inspect once.([1000])
IO.inspect once.([1000])
IO.inspect once.([1000])
IO.inspect once.([1000])

您将看到第一行在1秒后打印,但接下来的3行会立即打印,因为结果是从代理商处提取的。

答案 2 :(得分:1)

虽然两个已经给出的答案都是完全有效的,但您的javascript中最准确的翻译如下所示:

defmodule M do
  use GenServer

  def start_link(_opts \\ []) do
    GenServer.start_link(__MODULE__, nil, name: __MODULE__)
  end

  def init(_args) do
    Process.sleep(1_000)
    {:ok, 42}
  end

  def value() do
    start_link()
    GenServer.call(__MODULE__, :value)
  end

  def handle_call(:value, _from, state) do
    {:reply, state, state}
  end
end

(1..5) |> Enum.each(&IO.inspect(M.value(), label: to_string(&1)))

使用@ Dogbert答案中的相同指标:第一个值打印延迟,随后立即打印。

这与使用GenServer阶段的memoized函数完全类似。 GenServer.start_link/3返回以下内容之一:

{:ok, #PID<0.80.0>}
{:error, {:already_started, #PID<0.80.0>}}

那就是说,如果已经启动,就不会重启。我不打算检查返回的值,因为我们在任何情况下都设置了:如果它是初始启动,我们调用重函数,如果我们已经启动,则vaklue已经在state的指针处。