Funx.Monad.Writer
Mix.install([
{:funx, "0.4.0"}
])
Overview
The Funx.Monad.Writer module defines the Writer monad, which threads a log alongside a computed result.
Logs are accumulated using a Monoid implementation, injected lazily at runtime. This makes the Writer monad flexible and monoid-polymorphic—supporting lists, strings, or any user-defined monoid.
Core functions
-
pure/1– Wraps a result with an empty log. -
writer/1– Wraps a result and an explicit log. -
tell/1– Emits a log with no result. -
tap/2– Executes a side-effect function on the value without affecting the Writer’s value or log. -
listen/1– Returns both result and log as a pair. -
censor/2– Applies a function to transform the final log. -
pass/1– Uses a log-transforming function returned from within the computation. -
run/2– Executes the Writer and returns a%Writer.Result{}with result and log. -
eval/2– Executes and returns only the result. -
exec/2– Executes and returns only the log.
By default, the ListConcat monoid is used unless a different monoid is passed to run, eval, or exec.
This module also implements the Funx.Monad protocol.
Function Examples
import Funx.Monad.Writer
alias Funx.Monad
alias Funx.Tappable
alias Funx.Monad.Writer
pure/1
Wraps a value with no log.
writer = pure(42)
result = run(writer)
result.value
result.log
Monad.map/2
Applies a function to the value inside a Writer monad. The log is preserved.
This function is from the Funx.Monad module, not Funx.Monad.Writer.
writer = pure(5)
mapped = Monad.map(writer, fn x -> x * 2 end)
result = run(mapped)
result.value
result.log
Monad.bind/2
Applies a function that returns a Writer to the value inside a Writer monad. This is also known as “flatMap” in other languages.
The logs from both Writers are combined using the monoid.
This function is from the Funx.Monad module, not Funx.Monad.Writer.
writer = pure(5)
bound = Monad.bind(writer, fn x ->
tell([:computed, x])
|> Monad.bind(fn _ -> pure(x * 2) end)
end)
result = run(bound)
result.value
result.log
Monad.ap/2
Applies a function wrapped in a Writer to a value wrapped in a Writer.
The logs from both Writers are combined using the monoid.
This function is from the Funx.Monad module, not Funx.Monad.Writer.
func_writer = pure(&(&1 + 10))
value_writer = writer({7, [:initial]})
applied = Monad.ap(func_writer, value_writer)
result = run(applied)
result.value
result.log
writer/1
Wraps both a value and a raw log into the Writer context.
writer = writer({:ok, [:step1, :step2]})
result = run(writer)
result.value
result.log
tell/1
Appends a log value using the monoid, returning :ok as the result.
writer = tell([:event])
result = run(writer)
result.value
result.log
tap/2
Executes a side-effect function on the value without affecting the Writer’s value or log.
Important: tap does NOT add to the Writer’s log. Use tell/1 if you want to add to the log.
Useful for debugging, external logging, or side effects separate from the computation’s log.
# Side effect that doesn't add to log
writer =
pure(42)
|> Tappable.tap(&IO.inspect(&1, label: "debug"))
result = run(writer)
result.value
result.log # Empty - tap doesn't add to log!
# Compare with tell (adds to log)
writer =
pure(42)
|> Monad.bind(fn x ->
tell(["processed #{x}"])
|> Monad.map(fn _ -> x end)
end)
result = run(writer)
result.log # Contains ["processed 42"]
listen/1
Captures the current log and returns it alongside the result.
The log remains unchanged—only the result is modified to include it.
writer = writer({"done", [:start, :finish]})
listened = listen(writer)
result = run(listened)
result.value
result.log
censor/2
Transforms the final log by applying a function to it.
The result remains unchanged—only the log is modified.
writer = writer({"ok", [:a, :b]})
censored = censor(writer, fn log -> Enum.reverse(log) end)
result = run(censored)
result.value
result.log
pass/1
Applies a log-transforming function that is returned from within the computation.
This allows the result of a computation to include not only a value, but also a function that modifies the final accumulated log.
The input to pass/1 must be a Writer containing a tuple {result, f}, where
f is a function from log to log. This function will be applied to the final log
just before it’s returned.
result =
pure({"done", fn log -> log ++ [:transformed] end})
|> pass()
|> run()
result.value
result.log
run/2
Executes the Writer and returns both the result and the final accumulated log.
By default, it uses ListConcat unless a monoid is explicitly passed.
writer = writer({"ok", [:a, :b]})
result = run(writer)
result.value
result.log
exec/2
Executes the Writer and returns only the final accumulated log.
Uses ListConcat by default.
writer =
writer({:ok, [:step1]})
|> Monad.bind(fn _ -> tell([:step2]) end)
exec(writer)
eval/2
Executes the Writer and returns only the final result value.
Uses ListConcat by default.
writer =
writer({10, [:init]})
|> Monad.bind(fn x ->
tell([:logged])
|> Monad.bind(fn _ -> pure(x * 2) end)
end)
eval(writer)
Combining Writer Operations
computation =
pure(1)
|> Monad.bind(fn x ->
tell([:step1, x])
|> Monad.bind(fn _ ->
pure(x + 2)
|> Monad.bind(fn y ->
tell([:step2, y])
|> Monad.bind(fn _ ->
pure(y * 3)
|> Monad.bind(fn z ->
tell([:final, z])
|> Monad.map(fn _ -> z end)
end)
end)
end)
end)
end)
result = run(computation)
result.value
result.log
Using Different Monoids
You can use different monoids with Writer by passing them to run/2, eval/2, or exec/2:
# Using String concatenation monoid
string_writer =
pure("Hello")
|> Monad.bind(fn x ->
tell(" ")
|> Monad.bind(fn _ ->
tell("World")
|> Monad.map(fn _ -> x end)
end)
end)
# For demonstration, let's run with ListConcat (default)
result = run(string_writer)
result.log