在 Elixir 中编写函数 "once"
Writing the function "once" in Elixir
我来到 Elixir 主要是 Javascript 背景。在 JS 中,可以编写一个高阶函数 "once",其中 returns 一个只会调用传入函数一次的函数,并且 returns 是后续调用的先前结果 - 诀窍是操纵通过闭包捕获的变量:
var once = (func) => {
var wasCalled = false, prevResult;
return (...args) => {
if (wasCalled) return prevResult;
wasCalled = true;
return prevResult = func(...args);
}
}
在我看来,由于其不同的变量重新绑定行为,不可能在 Elixir 中创建此函数。有没有其他聪明的方法可以通过模式匹配或递归来做到这一点,还是根本不可能?如果没有宏,我想那些可能会启用它。谢谢
在大多数情况下,它不被认为是惯用的,但您可以使用 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 行是立即打印的,因为结果是从代理中获取的。
使用当前进程字典:
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
table:
# ... 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
/ Application.get_env
也可用于保存全局状态,但通常用于配置设置。
虽然已经给出的两个答案都完全有效,但您的 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
阶段的记忆函数的精确模拟。 GenServer.start_link/3
returns 以下之一:
{:ok, #PID<0.80.0>}
{:error, {:already_started, #PID<0.80.0>}}
也就是说,如果已经启动,不会重新启动。我懒得检查返回值,因为我们在任何情况下都已设置:如果它是初始启动,我们调用 heavy 函数,如果我们已经启动,则 vaklue 已经在 state
中。
我来到 Elixir 主要是 Javascript 背景。在 JS 中,可以编写一个高阶函数 "once",其中 returns 一个只会调用传入函数一次的函数,并且 returns 是后续调用的先前结果 - 诀窍是操纵通过闭包捕获的变量:
var once = (func) => {
var wasCalled = false, prevResult;
return (...args) => {
if (wasCalled) return prevResult;
wasCalled = true;
return prevResult = func(...args);
}
}
在我看来,由于其不同的变量重新绑定行为,不可能在 Elixir 中创建此函数。有没有其他聪明的方法可以通过模式匹配或递归来做到这一点,还是根本不可能?如果没有宏,我想那些可能会启用它。谢谢
在大多数情况下,它不被认为是惯用的,但您可以使用 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 行是立即打印的,因为结果是从代理中获取的。
使用当前进程字典:
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
table:
# ... 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
/ Application.get_env
也可用于保存全局状态,但通常用于配置设置。
虽然已经给出的两个答案都完全有效,但您的 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
阶段的记忆函数的精确模拟。 GenServer.start_link/3
returns 以下之一:
{:ok, #PID<0.80.0>}
{:error, {:already_started, #PID<0.80.0>}}
也就是说,如果已经启动,不会重新启动。我懒得检查返回值,因为我们在任何情况下都已设置:如果它是初始启动,我们调用 heavy 函数,如果我们已经启动,则 vaklue 已经在 state
中。