lib/postfix.ex

defmodule Postfix do
  @moduledoc """
  Documentation for `Postfix`.
  """

  @doc """
  Evaluate a list of values and functions

      iex> Postfix.eval([2, 3, &*/2, 4, &+/2])
      {:ok, 10}
  """
  @spec eval([term]) :: {:ok, term} | {:error, any}
  def eval([f]) when is_function(f) do
    eval_f(f)
  end

  def eval([v]) do
    {:ok, v}
  end

  def eval([f | rest]) when is_function(f) do
    with {:ok, r} <- eval_f(f) do
      eval([r | rest])
    end
  end

  def eval([v | [f | rest]]) when is_function(f) do
    with {:ok, r} <- eval_f(fn -> f.(v) end) do
      eval([r | rest])
    end
  end

  def eval([v1 | [v2 | [f | rest]]]) when is_function(f) do
    with {:ok, r} <- eval_f(fn -> f.(v1, v2) end) do
      eval([r | rest])
    end
  end

  def eval([v1 | [v2 | [v3 | [f | rest]]]]) when is_function(f) do
    with {:ok, r} <- eval_f(fn -> f.(v1, v2, v3) end) do
      eval([r | rest])
    end
  end

  def eval([v1 | [v2 | [v3 | [v4 | [f | rest]]]]]) when is_function(f) do
    with {:ok, r} <- eval_f(fn -> f.(v1, v2, v3, v4) end) do
      eval([r | rest])
    end
  end

  # Arrity 5..255
  def eval(terms) do
    not_function = fn x -> !is_function(x) end

    case Enum.split_while(terms, not_function) do
      {args, [f | rest]} ->
        with {:ok, r} <- eval_f(fn -> apply(f, args) end) do
          eval([r | rest])
        end

      {rest, []} ->
        {:ok, rest}
    end
  end

  defp eval_f(f) do
    case f.() do
      {:error, e} ->
        {:error, e}

      {:ok, v} ->
        {:ok, v}

      v ->
        {:ok, v}
    end
  end
end