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>
This commit is contained in:
348
docs/guide/06-handlers.md
Normal file
348
docs/guide/06-handlers.md
Normal file
@@ -0,0 +1,348 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user