Writing a once function in Elixir

I come to Elixir, primarily from a Javascript background. in JS, you can write a higher-order function "once", which returns a function that will call the passed function only once and returns the previous result on subsequent calls - the trick manipulates variables that were captured using 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 is impossible to create this function in Elixir because of its different rewriting behavior. Is there any other smart way to do this using pattern matching or recursion, or is it just not possible? Without macros, that is, I would suggest that they can enable it. Thanks

+4
source share
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:

# ... 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 , .

+3

, 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

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

, @Dogberts: , .

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

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

However, it does not restart if it is already running. I did not bother to check the return value, since we are all set up anyway: if its the initial start, we call a heavy function, if we were already running, vaklue is already under our fingers state.

+2
source

Source: https://habr.com/ru/post/1689728/


All Articles