🦀 Functional Rust

Example 1079: Writer Monad — Logging Computation

Difficulty: ⭐⭐⭐ Category: Monadic Patterns OCaml Source: https://cs3110.github.io/textbook/chapters/ds/monads.html

Problem Statement

Implement a Writer monad that accumulates a log of messages alongside a computation. Chain operations that produce both a result and log entries, combining them transparently.

Learning Outcomes

OCaml Approach

OCaml defines Writer as a record with `value` and `log` fields. The `>>=` operator (bind) applies a function to the value and concatenates the logs. `tell` creates a log-only entry. The pipeline reads naturally with `>>=` and `fun` closures.

Rust Approach

Rust implements Writer as a generic struct with `bind` and `map` methods. Method chaining (`.bind(half).bind(...)`) replaces OCaml's `>>=` operator. A generic version parameterized by a `Monoid` trait shows how the pattern generalizes beyond `Vec<String>`.

Key Differences

1. Operator overloading: OCaml defines `>>=` easily; Rust uses method chaining instead (operator overloading is possible but less ergonomic for monads) 2. Ownership: Rust's `bind` consumes `self`, making it clear the old Writer is gone. OCaml's bind copies/shares the log via GC 3. Monoid abstraction: OCaml uses `@` (list append) directly; Rust can abstract over the log type with a `Monoid` trait 4. Type inference: OCaml infers everything from usage; Rust sometimes needs explicit type annotations on closures
#[derive(Debug, Clone)]
struct Writer<A> {
    value: A,
    log: Vec<String>,
}

impl<A> Writer<A> {
    fn new(value: A) -> Self {
        Writer { value, log: Vec::new() }
    }

    fn bind<B, F>(self, f: F) -> Writer<B>
    where F: FnOnce(A) -> Writer<B> {
        let mut result = f(self.value);
        let mut combined = self.log;
        combined.append(&mut result.log);
        Writer { value: result.value, log: combined }
    }

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

fn tell(msg: impl Into<String>) -> Writer<()> {
    Writer { value: (), log: vec![msg.into()] }
}

fn half(x: i64) -> Writer<i64> {
    let result = x / 2;
    Writer { value: result, log: vec![format!("halved {x} to {result}")] }
}

fn compute(x: i64) -> Writer<i64> {
    Writer::new(x)
        .bind(half)
        .bind(|n| tell(format!("result is {n}")).map(|()| n))
}

fn main() {
    let result = compute(100);
    println!("Value: {}", result.value);
    for msg in &result.log {
        println!("  Log: {msg}");
    }
}

/* Output:
   Value: 50
     Log: halved 100 to 50
     Log: result is 50
*/
(* Writer Monad — Logging Computation *)

type 'a writer = { value: 'a; log: string list }

let return x = { value = x; log = [] }
let bind w f =
  let w' = f w.value in
  { value = w'.value; log = w.log @ w'.log }

let tell msg = { value = (); log = [msg] }
let ( >>= ) = bind

let half x =
  { value = x / 2; log = [Printf.sprintf "halved %d to %d" x (x / 2)] }

let compute x =
  return x >>= fun n ->
  half n >>= fun n ->
  tell (Printf.sprintf "result is %d" n) >>= fun () ->
  return n

let () =
  let result = compute 100 in
  assert (result.value = 50);
  assert (result.log = ["halved 100 to 50"; "result is 50"]);
  Printf.printf "Value: %d\n" result.value;
  List.iter (Printf.printf "  Log: %s\n") result.log;
  print_endline "ok"

📊 Detailed Comparison

OCaml vs Rust: Writer Monad — Logging Computation

Side-by-Side Code

OCaml

🐪 Show OCaml equivalent
type 'a writer = { value: 'a; log: string list }

let return x = { value = x; log = [] }
let bind w f =
let w' = f w.value in
{ value = w'.value; log = w.log @ w'.log }
let ( >>= ) = bind
let tell msg = { value = (); log = [msg] }

let half x =
{ value = x / 2; log = [Printf.sprintf "halved %d to %d" x (x / 2)] }

let compute x =
return x >>= fun n ->
half n >>= fun n ->
tell (Printf.sprintf "result is %d" n) >>= fun () ->
return n

Rust (idiomatic)

pub struct Writer<A> {
 pub value: A,
 pub log: Vec<String>,
}

impl<A> Writer<A> {
 pub fn new(value: A) -> Self { Writer { value, log: Vec::new() } }

 pub fn bind<B, F>(self, f: F) -> Writer<B>
 where F: FnOnce(A) -> Writer<B> {
     let mut result = f(self.value);
     let mut combined = self.log;
     combined.append(&mut result.log);
     Writer { value: result.value, log: combined }
 }
}

pub fn compute(x: i64) -> Writer<i64> {
 Writer::new(x)
     .bind(half)
     .bind(|n| tell(format!("result is {n}")).map(|()| n))
}

Rust (generic monoid)

pub trait Monoid: Default {
 fn combine(self, other: Self) -> Self;
}

pub struct GenericWriter<W, A> {
 pub value: A,
 pub log: W,
}

Type Signatures

ConceptOCamlRust
Writer type`type 'a writer = { value: 'a; log: string list }``struct Writer<A> { value: A, log: Vec<String> }`
Return/pure`val return : 'a -> 'a writer``fn new(value: A) -> Writer<A>`
Bind`val bind : 'a writer -> ('a -> 'b writer) -> 'b writer``fn bind<B>(self, f: FnOnce(A) -> Writer<B>) -> Writer<B>`
Tell`val tell : string -> unit writer``fn tell(msg: impl Into<String>) -> Writer<()>`

Key Insights

1. OCaml's `>>=` reads like a pipeline — `return x >>= half >>= ...` flows left-to-right. Rust's `.bind(half).bind(...)` achieves the same with method chaining. 2. Rust's `self` consumption is monadic by nature — `bind(self, f)` takes ownership, which mirrors the monad law that each bind transforms the entire computation, not just the value. 3. Log concatenation differs — OCaml uses `@` (list append, O(n)). Rust uses `Vec::append` which is amortized O(1) because it moves the buffer pointer. 4. The Monoid abstraction generalizes the pattern — by parameterizing the log type with a `Monoid` trait, Rust can use `String`, `Vec<T>`, or any accumulator. OCaml achieves this with module functors. 5. Both avoid side effects — the log is part of the return value, not a mutable global. This makes the computation pure and testable.

When to Use Each Style

Use Writer monad when: You need structured, composable logging that's part of the return type — audit trails, computation traces, query plan explanations. Use simple method chaining when: You just need basic logging and the full monadic abstraction is overkill — Rust's `log` crate or `tracing` is often simpler for real applications.