🦀 Functional Rust

189: Effect Handlers — Introduction

Difficulty: ⭐⭐⭐⭐⭐ Level: Expert Model algebraic effects as typed requests that a program emits and a handler fulfills — cleanly separating the description of "what to do" from the implementation of "how to do it."

The Problem This Solves

Side effects are normally baked into the function that produces them. `println!` reaches into stdout. `read_line` touches stdin. Testing a function that does both requires capturing stdout or injecting stdin — infrastructure that fights against the language's defaults. Mocking and dependency injection exist precisely to paper over this coupling. Algebraic effects (from languages like OCaml 5, Koka, Unison) propose a cleaner model: a computation performs an effect as a typed request — "I need a line of input" — without knowing how that request will be fulfilled. A nearby handler intercepts the request and decides: read from stdin, read from a test buffer, return a hardcoded value, log the request, whatever. The computation and the handler are completely decoupled. In Rust we don't have native effect syntax, but we can simulate the same structure: the program emits `Effect::Readline` or `Effect::Print(msg)` as data; a `Handler` trait intercepts each one. Swapping the handler changes the behavior without touching the program. The pure simulation handler and the real I/O handler implement the same trait.

The Intuition

Think of a waiter and a kitchen. The waiter takes orders (effects) from diners (the program). The waiter doesn't know how to cook — that's the kitchen's job (the handler). On a normal day, orders go to the real kitchen. During a health inspection, orders go to a simulation kitchen that records what was ordered without cooking anything. The waiter's behavior is identical in both cases; only the handler changes. In code: `interactive_program()` calls `readline_effect(...)` to say "I need a line of input here." It doesn't know whether that input comes from a real user or a test vector. The `PureHandler` provides fake inputs and captures outputs to a buffer. The `IoHandler` calls real `stdin.read_line`. The program is identical.

How It Works in Rust

// Step 1: Define the effect requests your program can make
enum Effect {
 Print(String),
 Readline,
}

// Step 2: What each effect "returns" to the program
enum EffectResult {
 Unit,
 Line(String),
}

// Step 3: A program step is either done or an effect + continuation
enum Step<A> {
 Done(A),
 Effect(Effect, Box<dyn FnOnce(EffectResult) -> Step<A>>),
 //             ^^ "after you handle this effect, give me the result, I'll continue"
}

// Step 4: Smart constructors build the description
fn readline_effect<A: 'static, F: FnOnce(String) -> Step<A> + 'static>(f: F) -> Step<A> {
 Step::Effect(
     Effect::Readline,
     Box::new(move |r| match r {
         EffectResult::Line(s) => f(s),  // receive the line, pass to continuation
         _ => panic!("unexpected"),
     }),
 )
}

fn print_effect<A: 'static>(msg: impl Into<String>, next: Step<A>) -> Step<A> {
 Step::Effect(Effect::Print(msg.into()), Box::new(move |_| next))
}

// Step 5: Define the Handler trait — one method per effect
trait Handler {
 fn handle_print(&mut self, msg: &str) -> EffectResult;
 fn handle_readline(&mut self) -> EffectResult;
}

// Step 6: Run the program by feeding effect results back into continuations
fn run_with<A, H: Handler>(mut handler: H, mut step: Step<A>) -> (A, H) {
 loop {
     match step {
         Step::Done(x) => return (x, handler),
         Step::Effect(Effect::Print(msg), cont) => {
             let r = handler.handle_print(&msg);
             step = cont(r);  // handler provides result, program continues
         }
         Step::Effect(Effect::Readline, cont) => {
             let r = handler.handle_readline();
             step = cont(r);
         }
     }
 }
}

// The SAME program, two different handlers:
fn interactive_program() -> Step<String> {
 print_effect("What is your name?",
     readline_effect(|name| {
         let greeting = format!("Hello, {}!", name);
         print_effect(greeting, done(name))
     }))
}

// Handler 1: pure test simulation
struct PureHandler { inputs: VecDeque<String>, output: String }
impl Handler for PureHandler {
 fn handle_print(&mut self, msg: &str) -> EffectResult {
     self.output.push_str(msg); self.output.push('\n');
     EffectResult::Unit
 }
 fn handle_readline(&mut self) -> EffectResult {
     EffectResult::Line(self.inputs.pop_front().unwrap_or_default())
 }
}

// Handler 2: real I/O — same interface
struct IoHandler;
impl Handler for IoHandler {
 fn handle_print(&mut self, msg: &str) -> EffectResult { println!("{}", msg); EffectResult::Unit }
 fn handle_readline(&mut self) -> EffectResult {
     let mut line = String::new();
     std::io::stdin().read_line(&mut line).unwrap();
     EffectResult::Line(line.trim_end().to_string())
 }
}

What This Unlocks

Key Differences

ConceptOCamlRust
Performing an effect`perform (Print "msg")` — built-in syntax, suspends execution`Step::Effect(Effect::Print(...), Box::new(continuation))` — manual, data-based
Resuming after effect`continue k value` — native continuation resumptionInterpreter calls `cont(result)` — continuation is a boxed closure
Handler syntax`match f () with \effect (Print msg) k -> ... \effect Readline k -> ...``impl Handler for Foo` — trait with one method per effect
Type safetyEffects declared with `effect Print : string -> unit`; type-checkedEffect enum is untyped; wrong EffectResult variant panics at runtime
Multiple effectsNative — same `match` handles multiple effectsMultiple `Effect` variants in one enum; single `Handler` trait with one method per variant
// Effect Handlers Intro — Simulated Algebraic Effects in Rust
//
// OCaml 5 has native algebraic effects.  Rust does not, but we can simulate
// the same pattern with the interpreter/visitor pattern:
//
//   * Define an "effect" as a request type.
//   * The "program" is a free-monad-like structure that issues requests.
//   * The "handler" is an interpreter that decides what to do for each request.
//
// DSL: Print a string / Readline — two effects.

// ── Effect requests ──────────────────────────────────────────────────────────

enum Effect {
    Print(String),
    Readline,
}

// ── A program step ───────────────────────────────────────────────────────────

enum Step<A> {
    Done(A),
    // Issue an effect; the continuation expects the "return value" of that effect.
    Effect(Effect, Box<dyn FnOnce(EffectResult) -> Step<A>>),
}

enum EffectResult {
    Unit,
    Line(String),
}

// ── Smart constructors ───────────────────────────────────────────────────────

fn done<A>(x: A) -> Step<A> {
    Step::Done(x)
}

fn print_effect<A: 'static>(msg: impl Into<String>, next: Step<A>) -> Step<A> {
    Step::Effect(
        Effect::Print(msg.into()),
        Box::new(move |_| next),
    )
}

fn readline_effect<A: 'static, F: FnOnce(String) -> Step<A> + 'static>(f: F) -> Step<A> {
    Step::Effect(
        Effect::Readline,
        Box::new(move |r| match r {
            EffectResult::Line(s) => f(s),
            _ => panic!("expected Line result"),
        }),
    )
}

fn bind_step<A: 'static, B: 'static, F: FnOnce(A) -> Step<B> + 'static>(
    step: Step<A>,
    f: F,
) -> Step<B> {
    match step {
        Step::Done(x) => f(x),
        Step::Effect(eff, cont) => {
            Step::Effect(eff, Box::new(move |r| bind_step(cont(r), f)))
        }
    }
}

// ── Handler trait ────────────────────────────────────────────────────────────

trait Handler {
    fn handle_print(&mut self, msg: &str) -> EffectResult;
    fn handle_readline(&mut self) -> EffectResult;
}

fn run_with<A, H: Handler>(mut handler: H, mut step: Step<A>) -> (A, H) {
    loop {
        match step {
            Step::Done(x) => return (x, handler),
            Step::Effect(Effect::Print(msg), cont) => {
                let r = handler.handle_print(&msg);
                step = cont(r);
            }
            Step::Effect(Effect::Readline, cont) => {
                let r = handler.handle_readline();
                step = cont(r);
            }
        }
    }
}

// ── Handler 1: pure simulation ────────────────────────────────────────────

struct PureHandler {
    inputs: std::collections::VecDeque<String>,
    output: String,
}

impl PureHandler {
    fn new(inputs: &[&str]) -> Self {
        PureHandler {
            inputs: inputs.iter().map(|s| s.to_string()).collect(),
            output: String::new(),
        }
    }
}

impl Handler for PureHandler {
    fn handle_print(&mut self, msg: &str) -> EffectResult {
        self.output.push_str(msg);
        self.output.push('\n');
        EffectResult::Unit
    }

    fn handle_readline(&mut self) -> EffectResult {
        EffectResult::Line(self.inputs.pop_front().unwrap_or_default())
    }
}

// ── Handler 2: real I/O ───────────────────────────────────────────────────

struct IoHandler;

impl Handler for IoHandler {
    fn handle_print(&mut self, msg: &str) -> EffectResult {
        println!("{}", msg);
        EffectResult::Unit
    }

    fn handle_readline(&mut self) -> EffectResult {
        let mut line = String::new();
        std::io::stdin().read_line(&mut line).unwrap();
        EffectResult::Line(line.trim_end().to_string())
    }
}

// ── Example program: interactive greeting ────────────────────────────────

fn interactive_program() -> Step<String> {
    print_effect(
        "What is your name?",
        readline_effect(|name| {
            let greeting = format!("Hello, {}!", name);
            print_effect(greeting, done(name))
        }),
    )
}

fn main() {
    // ── Pure simulation ───────────────────────────────────────────────
    let handler = PureHandler::new(&["OCaml 5"]);
    let (result, handler) = run_with(handler, interactive_program());
    println!("Output:\n{}", handler.output.trim());
    println!("Got name: {}", result);
    println!();

    // ── Real I/O handler (uncomment to test interactively) ────────────
    // let (_result, _) = run_with(IoHandler, interactive_program());
    println!("(Real I/O handler available — uncomment to use interactively)");
}

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

    #[test]
    fn test_greeting_output() {
        let handler = PureHandler::new(&["Rust"]);
        let (name, handler) = run_with(handler, interactive_program());
        assert_eq!(name, "Rust");
        assert!(handler.output.contains("Hello, Rust!"));
        assert!(handler.output.contains("What is your name?"));
    }

    #[test]
    fn test_pure_handler_no_real_io() {
        // Multiple readline calls
        let prog = readline_effect(|a| {
            readline_effect(move |b| done(format!("{} {}", a, b)))
        });
        let handler = PureHandler::new(&["hello", "world"]);
        let (result, _) = run_with(handler, prog);
        assert_eq!(result, "hello world");
    }

    #[test]
    fn test_print_collects_output() {
        let prog = bind_step(
            print_effect("line1", done(())),
            |_| print_effect("line2", done(42i32)),
        );
        let handler = PureHandler::new(&[]);
        let (val, h) = run_with(handler, prog);
        assert_eq!(val, 42);
        assert!(h.output.contains("line1"));
        assert!(h.output.contains("line2"));
    }
}
(* OCaml 5 algebraic effects: define effects, perform them, handle them.
   Like resumable exceptions with a continuation. *)

(* Note: requires OCaml 5.0+ *)

effect Print : string -> unit
effect Readline : string

(* A program that uses effects — pure in itself *)
let interactive_program () =
  perform (Print "What is your name?");
  let name = perform Readline in
  perform (Print ("Hello, " ^ name ^ "!"));
  name

(* Handler 1: real I/O *)
let run_io program =
  match program () with
  | result -> result
  | effect (Print msg) k ->
    print_endline msg;
    continue k ()
  | effect Readline k ->
    let line = input_line stdin in
    continue k line

(* Handler 2: pure simulation with list of inputs *)
let run_pure inputs program =
  let buf = Buffer.create 64 in
  let inputs = ref inputs in
  match program () with
  | result -> (result, Buffer.contents buf)
  | effect (Print msg) k ->
    Buffer.add_string buf (msg ^ "\n");
    continue k ()
  | effect Readline k ->
    let line = List.hd !inputs in
    inputs := List.tl !inputs;
    continue k line

let () =
  let (result, output) = run_pure ["OCaml 5"] interactive_program in
  Printf.printf "Output:\n%s" output;
  Printf.printf "Got name: %s\n" result