Files
lux/docs/guide/06-handlers.md
Brandon Lucas 44f88afcf8 docs: add comprehensive language documentation
Documentation structure inspired by Rust Book, Elm Guide, and others:

Guide (10 chapters):
- Introduction and setup
- Basic types (Int, String, Bool, List, Option, Result)
- Functions (closures, higher-order, composition)
- Data types (ADTs, pattern matching, records)
- Effects (the core innovation)
- Handlers (patterns and techniques)
- Modules (imports, exports, organization)
- Error handling (Fail, Option, Result)
- Standard library reference
- Advanced topics (traits, generics, optimization)

Reference:
- Complete syntax reference

Tutorials:
- Calculator (parsing, evaluation, REPL)
- Dependency injection (testing with effects)
- Project ideas (16 projects by difficulty)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-13 17:43:41 -05:00

7.1 KiB

Chapter 6: Handlers

Handlers are where effects become powerful. They define how effect operations behave, and they can do surprising things.

Basic Handler Structure

handler handlerName: EffectName {
    fn operation1(param1, param2) = {
        // Implementation
        resume(result)
    }
    fn operation2(param) = {
        // Implementation
        resume(result)
    }
}

Key points:

  • Handlers implement all operations of an effect
  • resume(value) continues the computation with value
  • Handlers can use other effects in their implementations

Understanding Resume

resume is the key to handler power. It continues the computation from where the effect was called.

effect Ask {
    fn ask(prompt: String): Int
}

fn computation(): Int with {Ask} = {
    let x = Ask.ask("first")   // Pauses here
    let y = Ask.ask("second")  // Then here
    x + y
}

handler doubler: Ask {
    fn ask(prompt) = resume(2)  // Always returns 2
}

run computation() with { Ask = doubler }
// Returns: 4 (2 + 2)

The flow:

  1. computation calls Ask.ask("first")
  2. Handler receives control, calls resume(2)
  3. computation continues with x = 2
  4. computation calls Ask.ask("second")
  5. Handler receives control, calls resume(2)
  6. computation continues with y = 2
  7. Returns 4

Handlers That Don't Resume

Not all handlers must resume. Some can abort:

effect Validate {
    fn check(condition: Bool, message: String): Unit
}

fn validateAge(age: Int): String with {Validate} = {
    Validate.check(age >= 0, "Age cannot be negative")
    Validate.check(age < 150, "Age seems unrealistic")
    "Valid age: " + toString(age)
}

handler strictValidator: Validate {
    fn check(cond, msg) =
        if cond then resume(())
        else "Validation failed: " + msg  // Returns early, doesn't resume
}

run validateAge(-5) with { Validate = strictValidator }
// Returns: "Validation failed: Age cannot be negative"

Handlers Using Other Effects

Handlers can use effects in their implementation:

effect Logger {
    fn log(msg: String): Unit
}

handler consoleLogger: Logger {
    fn log(msg) = {
        Console.print("[LOG] " + msg)  // Uses Console effect
        resume(())
    }
}

fn main(): Unit with {Console} = {
    let result = run {
        Logger.log("Hello")
        Logger.log("World")
        42
    } with { Logger = consoleLogger }
    Console.print("Result: " + toString(result))
}

Stateful Handlers

Handlers can maintain state using the State effect:

effect Counter {
    fn increment(): Unit
    fn get(): Int
}

handler counterHandler: Counter {
    fn increment() = {
        State.put(State.get() + 1)
        resume(())
    }
    fn get() = resume(State.get())
}

fn counting(): Int with {Counter} = {
    Counter.increment()
    Counter.increment()
    Counter.increment()
    Counter.get()
}

fn main(): Unit with {Console} = {
    let result = run {
        run counting() with { Counter = counterHandler }
    } with { State = 0 }
    Console.print("Count: " + toString(result))  // Count: 3
}

Handler Patterns

The Reader Pattern

Provide read-only context:

effect Config {
    fn get(key: String): String
}

handler envConfig: Config {
    fn get(key) = resume(Process.env(key))
}

handler mapConfig(settings: Map<String, String>): Config {
    fn get(key) = resume(Map.getOrDefault(settings, key, ""))
}

The Writer Pattern

Accumulate output:

effect Log {
    fn write(msg: String): Unit
}

handler collectLogs: Log {
    fn write(msg) = {
        State.put(State.get() + msg + "\n")
        resume(())
    }
}

fn program(): Int with {Log} = {
    Log.write("Starting")
    let result = 42
    Log.write("Done")
    result
}

// Get both result and logs
let (result, logs) = run {
    run program() with { Log = collectLogs }
    let logs = State.get()
    (result, logs)
} with { State = "" }

The Exception Pattern

Early termination with cleanup:

effect Fail {
    fn fail(msg: String): Unit
}

handler catchFail: Fail {
    fn fail(msg) = Err(msg)  // Don't resume, return error
}

fn riskyOperation(): Int with {Fail} = {
    if Random.bool() then Fail.fail("Bad luck!")
    else 42
}

let result: Result<Int, String> = run riskyOperation() with { Fail = catchFail }

The Choice Pattern

Non-determinism:

effect Choice {
    fn choose(options: List<T>): T
}

fn picker(): Int with {Choice} = {
    let x = Choice.choose([1, 2, 3])
    let y = Choice.choose([10, 20])
    x + y
}

// Handler that returns first option
handler firstChoice: Choice {
    fn choose(opts) = resume(List.head(opts))
}

// Handler that returns all combinations
handler allChoices: Choice {
    fn choose(opts) =
        List.flatMap(opts, fn(opt: T): List<T> => resume(opt))
}

Combining Multiple Handlers

Multiple effects, multiple handlers:

effect Logger { fn log(msg: String): Unit }
effect Counter { fn count(): Int }

fn program(): Int with {Logger, Counter} = {
    Logger.log("Starting")
    let n = Counter.count()
    Logger.log("Got " + toString(n))
    n * 2
}

handler myLogger: Logger {
    fn log(msg) = { Console.print(msg); resume(()) }
}

handler myCounter: Counter {
    fn count() = resume(42)
}

let result = run program() with {
    Logger = myLogger,
    Counter = myCounter
}

Handler Scope

Handlers apply to their run block:

handler loudLogger: Logger {
    fn log(msg) = { Console.print("!!! " + msg + " !!!"); resume(()) }
}

handler quietLogger: Logger {
    fn log(msg) = resume(())  // Silent
}

fn program(): Unit with {Logger, Console} = {
    Logger.log("Outer")

    let inner = run {
        Logger.log("Inner")
        42
    } with { Logger = quietLogger }

    Logger.log("Back to outer")
    Console.print("Inner result: " + toString(inner))
}

run program() with { Logger = loudLogger }
// Output:
// !!! Outer !!!
// !!! Back to outer !!!
// Inner result: 42

The inner run uses quietLogger, so "Inner" is silent.

Real-World Example: Database Testing

effect Database {
    fn query(sql: String): List<Row>
    fn execute(sql: String): Int
}

// Production handler using real database
handler postgresDb(conn: Connection): Database {
    fn query(sql) = resume(Postgres.query(conn, sql))
    fn execute(sql) = resume(Postgres.execute(conn, sql))
}

// Test handler using in-memory data
handler mockDb(data: List<Row>): Database {
    fn query(sql) = resume(data)
    fn execute(sql) = resume(1)
}

fn getUserCount(): Int with {Database} = {
    let rows = Database.query("SELECT COUNT(*) FROM users")
    extractCount(rows)
}

// Production
run getUserCount() with { Database = postgresDb(realConnection) }

// Testing - no database needed!
run getUserCount() with { Database = mockDb([Row { count: 42 }]) }

Summary

Pattern Use Case Resume?
Basic Provide implementation Yes
Early return Validation, errors No
Reader Configuration Yes
Writer Logging, accumulation Yes
State Counters, caches Yes
Exception Error handling Sometimes

Next

Chapter 7: Modules - Organizing code across files.