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>
349 lines
7.1 KiB
Markdown
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.
|