Powered by AppSignal & Oban Pro

Funx.Monad.Writer

livebooks/monad/writer/writer.livemd

Funx.Monad.Writer

Mix.install([
  {:funx, "~> 0.1.5"}
])

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.
  • 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.Writer

pure/1

Wraps a value with no log.

writer = pure(42)
result = run(writer)
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

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]})
  |> Funx.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]})
  |> Funx.Monad.bind(fn x ->
    tell([:logged])
    |> Funx.Monad.bind(fn _ -> pure(x * 2) end)
  end)
eval(writer)

Using Monad Protocol

The Writer monad implements the Funx.Monad protocol:

map/2

writer = pure(5)
mapped = Funx.Monad.map(writer, fn x -> x * 2 end)
result = run(mapped)
result.value

bind/2

writer = pure(5)
bound = Funx.Monad.bind(writer, fn x -> 
  tell([:computed, x])
  |> Funx.Monad.bind(fn _ -> pure(x * 2) end)
end)
result = run(bound)
result.value
result.log

ap/2

func_writer = pure(&(&1 + 10))
value_writer = writer({7, [:initial]})
applied = Funx.Monad.ap(func_writer, value_writer)
result = run(applied)
result.value
result.log

Combining Writer Operations

computation = 
  pure(1)
  |> Funx.Monad.bind(fn x ->
    tell([:step1, x])
    |> Funx.Monad.bind(fn _ ->
      pure(x + 2)
      |> Funx.Monad.bind(fn y ->
        tell([:step2, y])
        |> Funx.Monad.bind(fn _ ->
          pure(y * 3)
          |> Funx.Monad.bind(fn z ->
            tell([:final, z])
            |> Funx.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")
  |> Funx.Monad.bind(fn x ->
    tell(" ")
    |> Funx.Monad.bind(fn _ ->
      tell("World")
      |> Funx.Monad.map(fn _ -> x end)
    end)
  end)
# For demonstration, let's run with ListConcat (default)
result = run(string_writer)
result.log