lib/reather.ex

defmodule Reather do
  defstruct [:reather]

  @type reather :: %Reather{reather: fun()}

  require Reather.Macros
  alias Reather.Either

  @moduledoc """
  Reather is the combined form of Reader and Either monad.
  A `Reather` wrapps an environment and the child functions can
  use the environment to access the values.

  The evaluation of `Reather` is lazy, so it's never computed until
  explicitly call `Reather.run/2`.
  """

  defmacro __using__([]) do
    quote do
      import Kernel, except: [def: 2, defp: 2]
      import Reather.Macros, only: [reather: 1, def: 2, defp: 2]
      require Reather.Macros
      alias Reather.Either
    end
  end

  @doc """
  Get the current environment.
  """
  @spec ask :: reather
  def ask(), do: Reather.new(fn env -> {:ok, env} end)

  @doc """
  Run the reather.
  """
  @spec run(reather, %{}) :: any
  def run(%Reather{reather: fun}, env \\ %{}) do
    fun.(env)
  end

  @doc """
  Map a function to the reather.

  `map` is lazy, so it's never computed until explicitly call
  `Reather.run/2`.

  ## Examples
      iex> r = reather do
      ...>       x <- {:ok, 1}
      ...>       x
      ...>     end
      iex> r
      ...> |> Reather.map(fn x -> x + 1 end)
      ...> |> Reather.run()
      {:ok, 2}
  """
  @spec map(reather, (any -> any)) :: reather
  def map(r, fun) do
    Reather.Macros.reather do
      x <- r

      fun.(x)
    end
  end

  @doc """
  Transform a list of reathers to an reather of a list.

  This operation is lazy, so it's never computed until
  explicitly call `Reather.run/2`.

  ## Examples
      iex> r = [{:ok, 1}, {:ok, 2}, {:ok, 3}]
      ...>     |> Enum.map(&Reather.of/1)
      ...>     |> Reather.traverse()
      iex> Reather.run(r)
      {:ok, [1, 2, 3]}

      iex> r = [{:ok, 1}, {:error, "error"}, {:ok, 3}]
      ...>     |> Enum.map(&Reather.of/1)
      ...>     |> Reather.traverse()
      iex> Reather.run(r)
      {:error, "error"}
  """
  @spec traverse([reather]) :: reather
  def traverse(traversable) when is_list(traversable) do
    Reather.new(fn env ->
      traversable
      |> Enum.map(fn %Reather{} = r ->
        Reather.run(r, env)
      end)
      |> Either.traverse()
    end)
  end

  @doc """
  Inspect the reather result when run.
  """
  @spec inspect(reather, keyword) :: reather
  def inspect(%Reather{} = r, opts \\ []) do
    Reather.new(fn env ->
      r |> Reather.run(env) |> IO.inspect(opts)
    end)
  end

  @doc """
  Create a new `Reather` from the function.
  """
  @spec new(fun) :: reather
  def new(fun), do: %Reather{reather: fun}

  @doc """
  Create a `Reather` from the value.
  """
  @spec of(any) :: reather
  def of(v), do: Reather.new(fn _ -> Either.new(v) end)

  @doc """
  Create a `Reather` from the value.
  If the value is `Reather`, it will be returned as is.

  ## Examples
      iex> %Reather{} = Reather.wrap(:ok)

      iex> r = %Reather{}
      iex> ^r = Reather.wrap(r)
  """
  @spec wrap(any) :: reather
  def wrap(%Reather{} = r), do: r
  def wrap(v), do: of(v)

  @doc """
  Create a new `Reather` from a reather and function.
  The function will be called after the reather is run.
  """
  @spec chain(reather, (any -> reather)) :: reather
  def chain(%Reather{} = rhs, chain_fun) when is_function(chain_fun, 1) do
    Reather.new(fn env ->
      rhs
      |> Reather.run(env)
      |> case do
        {:ok, value} ->
          chain_fun.(value) |> Reather.run(env)

        {:error, _} = error ->
          error
      end
    end)
  end
end