lib/algae/reader.ex

defmodule Algae.Reader do
  @moduledoc ~S"""
  `Algae.Reader` allows you to pass some readable context around through actions.

  This is useful in a number of situations, but the most common case is to weave
  access to environment variables monadically.

  For an illustrated guide to `Reader`s,
  see [Thee Useful Monads](http://adit.io/posts/2013-06-10-three-useful-monads.html#the-state-monad).

  ## Examples

      iex> use Witchcraft
      ...>
      ...> correct =
      ...>   monad %Algae.Reader{} do
      ...>     count    <- ask &Map.get(&1, :count)
      ...>     bindings <- ask()
      ...>     return (count == Kernel.map_size(bindings))
      ...>   end
      ...>
      ...> sample_bindings = %{count: 3, a: 1, b: 2}
      ...> correct_count   = run(correct, sample_bindings)
      ...> "Correct count for %{a: 1, b: 2, count: 3}? true" == "Correct count for #{inspect sample_bindings}? #{correct_count}"
      true
      ...>
      ...> bad_bindings = %{count: 100, a: 1, b: 2}
      ...> bad_count    = run(correct, bad_bindings)
      ...> _ = "Correct count for #{inspect bad_bindings}? #{bad_count}"
      "Correct count for %{a: 1, b: 2, count: 100}? false"

  Example adapted from
  [source](https://hackage.haskell.org/package/mtl-2.2.1/docs/Control-Monad-Reader.html)

  """

  alias __MODULE__
  import Algae
  use Witchcraft

  defdata(fun())

  @doc """
  `Reader` constructor.

  ## Examples

      iex> newbie = new(fn x -> x * 10 end)
      ...> newbie.reader.(10)
      100

  """
  @spec new(fun()) :: t()
  def new(fun), do: %Reader{reader: fun}

  @doc """
  Run the reader function with some argument.

      iex> reader = new(fn x -> x + 5 end)
      ...> run(reader, 42)
      47

  This is the opposite of `new/1`.

      iex> fun = fn x -> x + 5 end
      ...> fun.(42) == fun |> new() |> run(42)
      true

  """
  @spec run(t(), any()) :: any()
  def run(%Reader{reader: fun}, arg), do: fun.(arg)

  @doc """
  Get the wrapped environment. Especially useful in monadic do-notation.

  ## Examples

      iex> run(ask(), 42)
      42

      iex> use Witchcraft
      ...>
      ...> example_fun =
      ...>   fn x ->
      ...>     monad %Algae.Reader{} do
      ...>       e <- ask()
      ...>       return {x, e}
      ...>     end
      ...>   end
      ...>
      ...> 42
      ...> |> example_fun.()
      ...> |> run(7)
      {42, 7}

  """
  @spec ask() :: t()
  def ask, do: Reader.new(fn x -> x end)

  @doc ~S"""
  Similar to `new/1` and `ask/0`. Construct an `Algae.Reader`,
  but apply a function to the constructed envoronment.

  The pun here is that you're "asking" a function for something.

  ## Examples

      iex> fn x -> x * 10 end
      ...> |> ask()
      ...> |> run(5)
      50

      iex> use Witchcraft
      ...>
      ...> foo =
      ...>   fn words ->
      ...>     monad %Algae.Reader{} do
      ...>       loud <- ask &(&1 == String.upcase(&1))
      ...>       return(words <> (if loud, do: "!", else: "."))
      ...>     end
      ...>   end
      ...>
      ...> "Hello" |> foo.() |> run("WORLD") # "WORLD" is the context being asked for
      "Hello!"

  """
  @spec ask((any() -> any())) :: t()
  def ask(fun) do
    monad %Reader{} do
      e <- ask
      return(fun.(e))
    end
  end

  @doc """
  Locally composes a function into a `Reader`.

  Often the idea is to temporarily adapt the `Reader` without continuing this
  change in later `run`s.

  ## Examples

      iex> ask()
      ...> |> local(fn word -> word <> "!" end)
      ...> |> local(&String.upcase/1)
      ...> |> run("o hai thar")
      "O HAI THAR!"

  """
  @spec local(t(), (any() -> any())) :: any()
  def local(reader, fun) do
    monad %Reader{} do
      e <- ask
      return(run(reader, fun.(e)))
    end
  end
end