defmodule Monad.Writer do @moduledoc """ The writer monad keeps track of a calculation and a "log". The log can be anything that conforms to the `Monoid` protocol. It's often useful to combine the writer monad with others. For example, you can use a `Monad.Maybe` as the `value` of the writer monad. This offers the benefit of having a log of a writer monad and the control flow of a maybe monad. """ use Monad.Behaviour @opaque t :: %__MODULE__{value: term, log: Monoid.t | :nil_monoid} @doc false defstruct value: nil, log: :nil_monoid @doc """ Wraps `value` into a writer monad. iex> writer 42 %Monad.Writer{value: 42, log: :nil_monoid} """ @spec writer(term) :: t def writer(value), do: %Monad.Writer{value: value} @doc """ Wraps `value` and `log` into a writer monad. iex> writer 42, "The answer" %Monad.Writer{value: 42, log: "The answer"} """ @spec writer(term, Monoid.t) :: t def writer(value, log), do: %Monad.Writer{value: value, log: log} @doc """ Returns the value and log from a writer monad. iex> w = writer 42, "The answer" iex> runWriter w {42, "The answer"} """ @spec runWriter(t) :: {term, Monoid.t} def runWriter(writer), do: {writer.value, writer.log} @doc """ Callback implementation of `Monad.Behaviour.return/1`. Wraps `value` into a writer monad. iex> return 42 %Monad.Writer{value: 42, log: :nil_monoid} """ @spec return(term) :: t def return(value), do: writer value @doc """ Callback implementation of `Monad.Behaviour.bind/2`. Unwraps the value from `writer` and applies it to `fun`. The log from `writer` and from the resulting writer monad are combined. iex> m = writer 4, ["Four"] iex> n = bind m, (& writer &1 * 2, ["Doubled"]) iex> runWriter n {8, ["Four", "Doubled"]} """ @spec bind(t, (term -> t)) :: t def bind(writer, fun) when is_function(fun, 1) do {val1, monoid1} = writer |> runWriter {val2, monoid2} = val1 |> fun.() |> runWriter writer val2, merge_monoids(monoid1, monoid2) end ## Helpers # `merge_monoids` is necessary because Elixir doesn't have a strong, static type system. # :nil_monoid basically acts like a universal "zero". defp merge_monoids(:nil_monoid, :nil_monoid), do: :nil_monoid defp merge_monoids(monoid, :nil_monoid), do: monoid defp merge_monoids(:nil_monoid, monoid), do: monoid defp merge_monoids(monoid1, monoid2), do: Monoid.mappend(monoid1, monoid2) end