Today there was a question raised on SO.

“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?”

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

The above is an example of this behaviour in JS, provided by OP. There were many different approaches given.

The first would be to use an Agent to store the value and lookup it before any subsequent execution.

Another one would be to use the Process dictionary when all the calls are done within the same process, or ETS/DETS for inter-process memoization.

I don’t think any of this would be an idiomatic approach. I would introduce the dedicated GenServer for this, that will run the heavy function inside it’s init callback:

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
    label =
      case start_link() do
        {:error, {:already_started, _}} -> "using memoized value: "
        {:ok, _} ->  "calculated the value: "
      end
    label <> GenServer.call(__MODULE__, :value)
  end

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

to check it’s working as expected, one might use:

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

#⇒ one second delay happens here
#  1: "calculated the value: 42"
#  2: "using memoized value: 42"
#  3: "using memoized value: 42"
#  4: "using memoized value: 42"
#  5: "using memoized value: 42"

One might notice, that the first value is printed with a delay, while all subsequent values are printed immediately.

This is an exact analog of the memoized function from JS, built using GenServer. GenServer.start_link/3 returns one of the following:

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

and this value is used in the code above to print the leading label. In the real life it might be just omitted:

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

The GenServer will not be resstarted if it’s already started. We can 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 the server was already started, the value is already at fingers in the state.

That might be turned into a helper module, like:

defmodule Memoized do
  defmacro __using__(opts) do
    with {:ok, fun} <- Keyword.fetch(opts, :fun),
         {:ok, name} <- Keyword.fetch(opts, :name) do
      block =
        quote do
          use GenServer

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

          def init(_args) do
            {:ok, unquote(fun).()}
          end

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

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

      quote do: Kernel.defmodule(unquote(name), do: unquote(block))
    end
  end
end

defmodule M do
  use Memoized, name: Fun, fun: fn -> Process.sleep(1_000); 42 end

  def check(), do: Enum.each(1..5, &IO.inspect(Fun.value(), label: to_string(&1)))
end

M.check()
#⇒ 1 second delay
#  1: 42
#  2: 42
#  3: 42
#  4: 42
#  5: 42

That’s it.