lib/algae/writer.ex

defmodule Algae.Writer do
  @moduledoc ~S"""
  `Algae.Writer` helps capture the pattern of writing to a pure log or accumulated
  value, handling the bookkeeping for you.

  If `Algae.Reader` is quasi-read-only, `Algae.Writer` is quasi-write-only.
  This is often used for loggers, but could be anything as long as the hidden value
  is a `Witchcraft.Monoid`.

  There are many applications of `Writer`s, but as an illustrative point,
  one could use it for logging across processes and time, since the log
  is carried around with the result in a pure fashion. The monadic DSL
  helps make using these feel more natural.

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

  ## Anatomy

        %Algae.Writer{writer: {value, log}}
                                 ↑     ↑
       # "explicit" value position    "hidden" position,
       #                               commonly used as a log

  ## Examples

      iex> use Witchcraft
      ...>
      ...> excite =
      ...>   fn string ->
      ...>     monad writer({0.0, "log"}) do
      ...>       tell string
      ...>
      ...>       excited <- return "#{string}!"
      ...>       tell " => #{excited} ... "
      ...>
      ...>       return excited
      ...>     end
      ...>   end
      ...>
      ...> {_, logs} =
      ...>   "Hi"
      ...>   |> excite.()
      ...>   >>> excite
      ...>   >>> excite
      ...>   |> censor(&String.trim_trailing(&1, " ... "))
      ...>   |> run()
      ...>
      ...> logs
      "Hi => Hi! ... Hi! => Hi!! ... Hi!! => Hi!!!"

      iex> use Witchcraft
      ...>
      ...> exponent =
      ...>   fn num ->
      ...>     monad writer({0, 0}) do
      ...>       tell 1
      ...>       return num * num
      ...>     end
      ...>   end
      ...>
      ...> initial = 42
      ...> {result, times} = run(exponent.(initial) >>> exponent >>> exponent)
      ...>
      ...> "#{initial}^#{round(:math.pow(2, times))} = #{result}"
      "42^8 = 9682651996416"

  """

  alias __MODULE__
  alias Witchcraft.{Monoid, Unit}
  use   Witchcraft

  @type log    :: Monoid.t()
  @type value  :: any()
  @type writer :: {Writer.value(), Writer.log()}

  @type t :: %Writer{writer: writer()}

  defstruct writer: {0, []}

  @doc """
  Construct a `Algae.Writer` struct from a starting value and log.

  ## Examples

      iex> new()
      %Algae.Writer{writer: {0, []}}

      iex> new("hi")
      %Algae.Writer{writer: {"hi", []}}

      iex> new("ohai", 42)
      %Algae.Writer{writer: {"ohai", 42}}

  """
  @spec new(any(), Monoid.t()) :: Writer.t()
  def new(value \\ 0, log \\ []), do: %Writer{writer: {value, log}}

  @doc """
  Similar to `new/2`, but taking a tuple rather than separate fields.

  ## Examples

      iex> writer({"ohai", 42})
      %Algae.Writer{writer: {"ohai", 42}}

  """
  @spec writer(Writer.writer()) :: Writer.t()
  def writer({value, log}), do: new(value, log)

  @doc ~S"""
  Extract the enclosed value and log from an `Algae.Writer`.

  ## Examples

      iex> run(%Algae.Writer{writer: {"hi", "there"}})
      {"hi", "there"}

      iex> use Witchcraft
      ...>
      ...> half =
      ...>   fn num ->
      ...>     monad writer({0.0, ["log"]}) do
      ...>       let half = num / 2
      ...>       tell ["#{num} / 2 = #{half}"]
      ...>       return half
      ...>     end
      ...>   end
      ...>
      ...> run(half.(42) >>> half >>> half)
      {
        5.25,
        [
          "42 / 2 = 21.0",
          "21.0 / 2 = 10.5",
          "10.5 / 2 = 5.25"
        ]
      }

  """
  @spec run(Writer.t()) :: Writer.value()
  def run(%Writer{writer: writer}), do: writer

  @doc ~S"""
  Set the "log" portion of an `Algae.Writer` step

  ## Examples

      iex> tell("secrets")
      %Algae.Writer{writer: {%Witchcraft.Unit{}, "secrets"}}

      iex> use Witchcraft
      ...>
      ...> monad %Algae.Writer{writer: {"string", 1}} do
      ...>   tell 42
      ...>   tell 43
      ...>   return "hey"
      ...> end
      %Algae.Writer{writer: {"hey", 85}}

      iex> use Witchcraft
      ...>
      ...> half =
      ...>   fn num ->
      ...>     monad writer({0.0, ["log"]}) do
      ...>       let half = num / 2
      ...>       tell ["#{num} / 2 = #{half}"]
      ...>       return half
      ...>     end
      ...>   end
      ...>
      ...> run(half.(42.0) >>> half >>> half)
      {
        5.25,
        [
          "42.0 / 2 = 21.0",
          "21.0 / 2 = 10.5",
          "10.5 / 2 = 5.25"
        ]
      }

  """
  @spec tell(Writer.log()) :: Writer.t()
  def tell(log), do: new(%Unit{}, log)

  @doc """
  Copy the log into the value position. This makes it accessible in do-notation.

  ## Examples

      iex> listen(%Algae.Writer{writer: {42, "hi"}})
      %Algae.Writer{writer: {{42, "hi"}, "hi"}}

      iex> use Witchcraft
      ...>
      ...> monad new(1, 1) do
      ...>   wr <- listen tell(42)
      ...>   tell 43
      ...>   return wr
      ...> end
      %Algae.Writer{
        writer: {{%Witchcraft.Unit{}, 42}, 85}
      }

  """
  @spec listen(Writer.t()) :: Writer.t()
  def listen(%Writer{writer: {value, log}}), do: %Writer{writer: {{value, log}, log}}

  @doc """
  Similar to `listen/1`, but with the ability to adjust the copied log.

  ## Examples

      iex> listen(%Algae.Writer{writer: {1, "hi"}}, &String.upcase/1)
      %Algae.Writer{
        writer: {{1, "HI"}, "hi"}
      }

  """
  @spec listen(Writer.t(), (log() -> log())) :: Writer.t()
  def listen(writer, fun) do
    monad writer do
      {value, log} <- listen writer
      return {value, fun.(log)}
    end
  end

  @doc ~S"""
  Run a function in the value portion of an `Algae.Writer` on the log.

  Notice that the structure is similar to what somes out of `listen/{1,2}`

      Algae.Writer{writer: {{_, function}, log}}

  ## Examples

      iex> pass(%Algae.Writer{writer: {{1, fn x -> x * 10 end}, 42}})
      %Algae.Writer{writer: {1, 420}}

      iex> use Witchcraft
      ...>
      ...> monad new("string", ["logs"]) do
      ...>   a <-  ["start"] |> tell() |> listen()
      ...>   tell ["middle"]
      ...>
      ...>   {value, logs} <- return a
      ...>   pass writer({{value, fn [log | _] -> [log | [log | logs]] end}, logs})
      ...>
      ...>   tell ["next is 42"]
      ...>   return 42
      ...> end
      %Algae.Writer{
        writer: {42, ["start", "middle", "start", "start", "start", "next is 42"]}
      }

  """
  @spec pass(Writer.t()) :: Writer.t()
  def pass(%Writer{writer: {{value, fun}, log}}), do: %Writer{writer: {value, fun.(log)}}

  @doc ~S"""
  Run a writer, and run a function over the resulting log.

  ## Examples

      iex> 42
      ...> |> new(["hi", "THERE", "friend"])
      ...> |> censor(&Enum.reject(&1, fn log -> String.upcase(log) == log end))
      ...> |> run()
      {42, ["hi", "friend"]}

      iex> use Witchcraft
      ...>
      ...> 0
      ...> |> new(["logs"])
      ...> |> monad do
      ...>   tell ["Start"]
      ...>   tell ["BANG!"]
      ...>   tell ["shhhhhhh..."]
      ...>   tell ["LOUD NOISES!!!"]
      ...>   tell ["End"]
      ...>
      ...>   return 42
      ...> end
      ...> |> censor(&Enum.reject(&1, fn log -> String.upcase(log) == log end))
      ...> |> run()
      {42, ["Start", "shhhhhhh...", "End"]}

  """
  @spec censor(Writer.t(), (any() -> any())) :: Writer.t()
  def censor(writer, fun) do
    pass(monad writer do
      value <- writer
      return {value, fun}
    end)
  end
end