ExamplesBy LevelBy TopicLearning Paths
861 Expert

Writer Monad

Functional Programming

Tutorial

The Problem

Functions that need to accumulate a log, collect diagnostics, or build an audit trail alongside their computation result face a choice: return a tuple (result, log) or use a mutable global. The Writer monad encapsulates the log accumulation: Writer<A> represents a value A paired with a log Vec<String>. Computations are composed and their logs automatically concatenated. This separates concerns: the core logic doesn't know about logging; the monad handles it. Use cases: compiler diagnostics (warnings alongside the compiled output), query plan logging, audit trails, and trace accumulation in distributed tracing.

🎯 Learning Outcomes

  • • Understand Writer<A> as (A, Vec<String>) — a value paired with an accumulated log
  • • Implement tell(msg) to emit a log entry, pure(x) to lift a value with empty log
  • • Implement monadic bind: concatenate the two computations' logs
  • • Recognize the constraint: log must form a monoid (empty log + concatenation)
  • • Apply to: audit logging, compiler warnings, distributed trace accumulation
  • Code Example

    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] } }
    }

    Key Differences

    AspectRustOCaml
    Log typeVec<String>string list
    Log concatenation[log1, log2].concat()log1 @ log2
    pureWriter::pure(a){ value = a; log = [] }
    tellWriter::tell(msg){ value = (); log = [msg] }
    Monoid constraintVec<String> (implicit)string list or explicit
    PerformanceO(n) per appendO(n) per @

    OCaml Approach

    OCaml represents Writer as type 'a writer = { value: 'a; log: string list }. pure a = { value = a; log = [] }. tell msg = { value = (); log = [msg] }. Bind: let bind w f = let w2 = f w.value in { value = w2.value; log = w.log @ w2.log }. OCaml's @ operator appends lists. The Writer monad requires the log type to be a monoid ([] for empty, @ for combine). For performance, Buffer.t or Queue.t replaces immutable list append. OCaml's let%bind with ppx_writer provides do-notation.

    Full Source

    #![allow(clippy::all)]
    // 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"]);
        }
    }
    ✓ Tests Rust test suite
    #[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"]);
        }
    }

    Deep Comparison

    Comparison: Writer Monad

    Writer Type

    OCaml:

    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:

    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:

    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))
    

    Exercises

  • Implement a computation that factors a number and logs each factor found using the Writer monad.
  • Replace Vec<String> with a String buffer and implement a writer that builds a formatted log string.
  • Generalize Writer to Writer<A, W> where W must implement a Monoid trait with empty() and combine.
  • Use the Writer monad to collect performance timings alongside computation results.
  • Implement sequence(writers: Vec<Writer<A>>) -> Writer<Vec<A>> that runs all writers and concatenates their logs.
  • Open Source Repos