简体   繁体   中英

Writing the function “once” in Elixir

I'm coming to Elixir from primarily a Javascript background. in JS, it's possible to write a higher order function "once" which returns a function that will invoke the passed in function only once, and returns the previous result on subsequent calls- the trick is manipulating variables that were captured via closure:

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

It seems to me that it's not possible to create this function in Elixir, due to its different variable rebinding behavior. Is there some other clever way to do it via pattern matching or recursion, or is it just not possible? Without macros that is, I'd imagine those might enable it. Thanks

Using the current process dictionary:

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

Or if the function will be passed across processes, an ets table can be used:

# ... 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 can also be used to hold global state, though usually is used for configuration settings.

It's not considered idiomatic in most cases, but you can do this with 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

Now if you run this:

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

You'll see that the first line is printed after 1 second, but the next 3 are printed instantly, because the result is fetched from the agent.

While both already given answers are perfectly valid, the most precise translation from your javascript is shown below:

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

Use the same metric as in @Dogbert's answer: the first value is printed with a delay, all subsequent are printed immediately.

This is an exact analog of your memoized function using GenServer stage. GenServer.start_link/3 returns one of the following:

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

That said, it is not restarted if it's already started. I do not bother to check the returned value since we are all set in any case: if it's the initial start, we call the heavy function, if we were already started, the vaklue is already at fingers in the state .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM