๐Ÿฆ€ Functional Rust

062: Writer Monad

Difficulty: 3 Level: Advanced Accumulate a log or audit trail alongside a computation without passing a log buffer to every function.

The Problem This Solves

You're building a computation pipeline. You want to log what happened at each step โ€” for debugging, for auditing, for observability. The simplest approach: add `log: &mut Vec<String>` to every function signature.
fn add(x: i32, y: i32, log: &mut Vec<String>) -> i32 {
 log.push(format!("Adding {} + {} = {}", x, y, x + y));
 x + y
}

fn multiply(x: i32, y: i32, log: &mut Vec<String>) -> i32 {
 log.push(format!("Multiplying {} * {} = {}", x, y, x * y));
 x * y
}

fn compute(a: i32, b: i32, c: i32, log: &mut Vec<String>) -> i32 {
 let sum     = add(a, b, log);
 let product = multiply(sum, c, log);
 product
}
This works. But now the log buffer is tangled into every function signature. Your `add` function is no longer a pure math operation โ€” it has a side effect dependency. Testing `add` in isolation requires creating a log buffer even if you don't care about the log in that test. Refactoring from `Vec<String>` to a structured log type means touching every function signature. The Writer monad decouples the accumulation from the computation. Each function returns `(value, log_entries)` โ€” a pair. The Writer wrapper automatically merges the log entries as you chain functions, so you never thread `&mut log` through anything. The final result carries the complete log, and you read it off at the end. Conceptually: instead of `fn f(x) -> A`, every step is `fn f(x) -> (A, Log)`. Writer makes chaining these pairs clean. This exists to solve exactly that pain.

The Intuition

Imagine every step of your computation is a machine that produces two outputs: the result and a sticky note describing what it did. You chain the machines: the result of machine A flows into machine B, but the sticky notes from both get stacked together. At the end, you have the final result and a complete stack of sticky notes describing everything that happened.
[Machine A: add 3+4] โ†’ value: 7,  note: "Adding 3 + 4 = 7"
[Machine B: mul 7ร—2] โ†’ value: 14, note: "Multiplying 7 * 2 = 14"

Combined result: value=14, log=["Adding 3 + 4 = 7", "Multiplying 7 * 2 = 14"]
You never had to carry the sticky note stack manually โ€” the Writer chain handled it. Jargon decoded:

How It Works in Rust

// Writer carries both a value and an accumulated log
#[derive(Debug, Clone)]
struct Writer<A> {
 value: A,
 log: Vec<String>,
}

impl<A> Writer<A> {
 // Wrap a plain value โ€” log starts empty
 fn pure(a: A) -> Self {
     Writer { value: a, log: vec![] }
 }

 // Emit a log entry โ€” value is ()
 fn tell(msg: String) -> Writer<()> {
     Writer { value: (), log: vec![msg] }
 }

 // Chain: run f on value, merge both logs together
 fn and_then<B>(self, f: impl FnOnce(A) -> Writer<B>) -> Writer<B> {
     let Writer { value: b, log: log2 } = f(self.value);
     let mut log = self.log;
     log.extend(log2);          // merge: self's log + f's log
     Writer { value: b, log }
 }

 // Transform value without changing the log
 fn map<B>(self, f: impl FnOnce(A) -> B) -> Writer<B> {
     Writer { value: f(self.value), log: self.log }
 }
}
// Building a logged computation:
fn add_with_log(x: i32, y: i32) -> Writer<i32> {
 Writer::tell(format!("Adding {} + {}", x, y))
     .and_then(move |()| {
         let sum = x + y;
         Writer::tell(format!("Result: {}", sum))
             .map(move |()| sum)      // attach the value after logging
     })
}

fn multiply_with_log(x: i32, y: i32) -> Writer<i32> {
 Writer::tell(format!("Multiplying {} * {}", x, y))
     .map(move |()| x * y)            // log first, then produce value
}
// Chain them: logs accumulate automatically
fn computation() -> Writer<i32> {
 add_with_log(3, 4)
     .and_then(|sum| multiply_with_log(sum, 2))
     .and_then(|product| {
         Writer::tell("Done!".to_string()).map(move |()| product)
     })
}

let w = computation();
println!("Result: {}", w.value);  // 14
println!("Log: {:?}", w.log);
// ["Adding 3 + 4", "Result: 7", "Multiplying 7 * 2", "Done!"]
// All steps logged, nothing passed manually
// Writer as a general accumulator โ€” collect filtered values:
fn gather_evens(xs: &[i32]) -> Writer<()> {
 xs.iter().fold(Writer::pure(()), |acc, &x| {
     acc.and_then(move |()| {
         if x % 2 == 0 {
             Writer { value: (), log: vec![format!("{}", x)] }
         } else {
             Writer::pure(())  // odd numbers contribute nothing
         }
     })
 })
}

let evens = gather_evens(&[1, 2, 3, 4, 5, 6]);
println!("{:?}", evens.log);  // ["2", "4", "6"]
// Writer used as a collector, not just a logger

What This Unlocks

Key Differences

ConceptOCamlRust
Type`type ('a, 'w) writer = 'a 'w` (tuple, no wrapper needed)`struct Writer<A> { value: A, log: Vec<String> }`
Log appending`@` operator (list concatenation, immutable)`Vec::extend` (mutate-in-place, more efficient)
Generic log type`('a, 'w) writer` where `'w` is any monoidTypically specialized to `Vec<String>`; generic version needs a `Monoid` trait (not in stdlib)
`tell``tell msg = ((), [msg])``Writer::tell(msg)` โ€” creates `Writer { value: (), log: vec![msg] }`
Ownership of logImmutable lists shared freely via GCLog vector is moved* through the chain โ€” each `extend` consumes the old log
Stdlib support?NoNo โ€” must implement; `tracing` crate solves this in production
// Example 062: Writer Monad
// Accumulate a log alongside computation results

// Approach 1: Writer struct with Vec<String> log
#[derive(Debug, Clone)]
struct Writer<A> {
    value: A,
    log: Vec<String>,
}

impl<A> Writer<A> {
    fn pure(a: A) -> Self {
        Writer { value: a, log: vec![] }
    }

    fn and_then<B>(self, f: impl FnOnce(A) -> Writer<B>) -> Writer<B> {
        let Writer { value: b, log: log2 } = f(self.value);
        let mut log = self.log;
        log.extend(log2);
        Writer { value: b, log }
    }

    fn map<B>(self, f: impl FnOnce(A) -> B) -> Writer<B> {
        Writer { value: f(self.value), log: self.log }
    }
}

/// tell as a free function returning Writer<()>
fn tell(msg: String) -> Writer<()> {
    Writer { value: (), log: vec![msg] }
}

fn add_with_log(x: i32, y: i32) -> Writer<i32> {
    tell(format!("Adding {} + {}", x, y))
        .and_then(move |()| {
            let sum = x + y;
            tell(format!("Result: {}", sum))
                .map(move |()| sum)
        })
}

fn multiply_with_log(x: i32, y: i32) -> Writer<i32> {
    tell(format!("Multiplying {} * {}", x, y))
        .map(move |()| x * y)
}

fn computation() -> Writer<i32> {
    add_with_log(3, 4)
        .and_then(|sum| multiply_with_log(sum, 2))
        .and_then(|product| {
            tell("Done!".to_string()).map(move |()| product)
        })
}

// Approach 2: Generic Writer with any monoid-like log
#[derive(Debug)]
struct WriterG<W, A> {
    value: A,
    log: W,
}

impl<A> WriterG<String, A> {
    fn str_pure(a: A) -> Self {
        WriterG { value: a, log: String::new() }
    }

    fn str_bind<B>(self, f: impl FnOnce(A) -> WriterG<String, B>) -> WriterG<String, B> {
        let w2 = f(self.value);
        WriterG { value: w2.value, log: self.log + &w2.log }
    }
}

fn str_tell(msg: &str) -> WriterG<String, ()> {
    WriterG { value: (), log: msg.to_string() }
}

// Approach 3: Collect values (Writer as accumulator)
fn gather_evens(xs: &[i32]) -> Writer<()> {
    xs.iter().fold(Writer::pure(()), |acc, &x| {
        acc.and_then(move |()| {
            if x % 2 == 0 {
                Writer { value: (), log: vec![format!("{}", x)] }
            } else {
                Writer::pure(())
            }
        })
    })
}


#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_computation() {
        let w = computation();
        assert_eq!(w.value, 14);
        assert_eq!(w.log.len(), 4);
        assert!(w.log[0].contains("Adding 3 + 4"));
    }

    #[test]
    fn test_pure_empty_log() {
        let w: Writer<i32> = Writer::pure(42);
        assert_eq!(w.value, 42);
        assert!(w.log.is_empty());
    }

    #[test]
    fn test_tell() {
        let w = tell("hello".into());
        assert_eq!(w.log, vec!["hello"]);
    }

    #[test]
    fn test_gather_evens() {
        let w = gather_evens(&[1, 2, 3, 4, 5, 6]);
        assert_eq!(w.log, vec!["2", "4", "6"]);
    }

    #[test]
    fn test_map() {
        let w = Writer::pure(5).map(|x: i32| x * 2);
        assert_eq!(w.value, 10);
        assert!(w.log.is_empty());
    }

    #[test]
    fn test_and_then_combines_logs() {
        let w = tell("a".into())
            .and_then(|()| tell("b".into()));
        assert_eq!(w.log, vec!["a", "b"]);
    }
}
(* Example 062: Writer Monad *)
(* Accumulate a log alongside computation results *)

type ('w, 'a) writer = Writer of ('a * 'w)

let run_writer (Writer (a, w)) = (a, w)
let return_ x = Writer (x, [])
let bind (Writer (a, w1)) f =
  let Writer (b, w2) = f a in
  Writer (b, w1 @ w2)
let ( >>= ) = bind
let tell w = Writer ((), [w])

(* Approach 1: Logging computation steps *)
let add_with_log x y =
  tell (Printf.sprintf "Adding %d + %d" x y) >>= fun () ->
  let sum = x + y in
  tell (Printf.sprintf "Result: %d" sum) >>= fun () ->
  return_ sum

let multiply_with_log x y =
  tell (Printf.sprintf "Multiplying %d * %d" x y) >>= fun () ->
  return_ (x * y)

let computation =
  add_with_log 3 4 >>= fun sum ->
  multiply_with_log sum 2 >>= fun product ->
  tell "Done!" >>= fun () ->
  return_ product

(* Approach 2: Writer with monoid (string concatenation) *)
type 'a str_writer = StrWriter of ('a * string)

let str_tell s = StrWriter ((), s)
let str_return x = StrWriter (x, "")
let str_bind (StrWriter (a, w1)) f =
  let StrWriter (b, w2) = f a in
  StrWriter (b, w1 ^ w2)

(* Approach 3: Writer for collecting values *)
let collect x = Writer ((), [x])

let gather_evens xs =
  List.fold_left (fun acc x ->
    acc >>= fun () ->
    if x mod 2 = 0 then collect x
    else return_ ()
  ) (return_ ()) xs

let () =
  let (result, log) = run_writer computation in
  assert (result = 14);
  assert (List.length log = 3);
  assert (List.hd log = "Adding 3 + 4");

  let ((), evens) = run_writer (gather_evens [1;2;3;4;5;6]) in
  assert (evens = [2; 4; 6]);

  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed Comparison

Comparison: Writer Monad

Writer Type

OCaml:

๐Ÿช Show OCaml equivalent
type ('w, 'a) writer = Writer of ('a * 'w)
let tell w = Writer ((), [w])
let return_ x = Writer (x, [])

Rust:

struct Writer<A> { value: A, log: Vec<String> }
impl<A> Writer<A> {
 fn pure(a: A) -> Self { Writer { value: a, log: vec![] } }
 fn tell(msg: String) -> Writer<()> { Writer { value: (), log: vec![msg] } }
}

Bind (Log Accumulation)

OCaml:

๐Ÿช Show OCaml equivalent
let bind (Writer (a, w1)) f =
let Writer (b, w2) = f a in
Writer (b, w1 @ w2)    (* list append *)

Rust:

fn and_then<B>(self, f: impl FnOnce(A) -> Writer<B>) -> Writer<B> {
 let w2 = f(self.value);
 let mut log = self.log;
 log.extend(w2.log);    // vec extend
 Writer { value: w2.value, log }
}

Logged Computation

OCaml:

๐Ÿช Show OCaml equivalent
add_with_log 3 4 >>= fun sum ->
multiply_with_log sum 2 >>= fun product ->
tell "Done!" >>= fun () ->
return_ product

Rust:

add_with_log(3, 4)
 .and_then(|sum| multiply_with_log(sum, 2))
 .and_then(|product| Writer::tell("Done!".into()).map(move |()| product))