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