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

349 lines
7.1 KiB
Markdown

# Chapter 6: Handlers
Handlers are where effects become powerful. They define *how* effect operations behave, and they can do surprising things.
## Basic Handler Structure
```lux
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.
```lux
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:
```lux
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:
```lux
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:
```lux
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:
```lux
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:
```lux
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:
```lux
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:
```lux
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:
```lux
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:
```lux
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
```lux
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](07-modules.md) - Organizing code across files.