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