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>
351 lines
7.3 KiB
Markdown
351 lines
7.3 KiB
Markdown
# Chapter 5: Effects
|
|
|
|
This is where Lux gets interesting. Effects are the core innovation—they make side effects explicit, controllable, and composable.
|
|
|
|
## The Problem with Side Effects
|
|
|
|
In most languages, functions can do *anything*:
|
|
|
|
```javascript
|
|
// JavaScript - what does this do?
|
|
function process(x) {
|
|
return x * 2;
|
|
}
|
|
```
|
|
|
|
It looks pure, but it could:
|
|
- Print to console
|
|
- Write to a file
|
|
- Make HTTP requests
|
|
- Throw exceptions
|
|
- Modify global state
|
|
- Launch missiles
|
|
|
|
You can't tell from the signature. You have to read the implementation.
|
|
|
|
## The Lux Solution
|
|
|
|
In Lux, effects are declared:
|
|
|
|
```lux
|
|
// Pure - only computes
|
|
fn double(x: Int): Int = x * 2
|
|
|
|
// Uses Console - you can see it
|
|
fn printDouble(x: Int): Unit with {Console} =
|
|
Console.print(toString(x * 2))
|
|
```
|
|
|
|
The `with {Console}` tells you this function interacts with the console. It's part of the type.
|
|
|
|
## Built-in Effects
|
|
|
|
Lux provides several built-in effects:
|
|
|
|
| Effect | Operations | Purpose |
|
|
|--------|------------|---------|
|
|
| `Console` | `print`, `readLine`, `readInt` | Terminal I/O |
|
|
| `Fail` | `fail` | Early termination |
|
|
| `State` | `get`, `put` | Mutable state |
|
|
| `Random` | `int`, `float`, `bool` | Random numbers |
|
|
| `File` | `read`, `write`, `exists` | File system |
|
|
| `Process` | `exec`, `env`, `args` | System processes |
|
|
| `Http` | `get`, `post`, `put`, `delete` | HTTP client |
|
|
|
|
Example usage:
|
|
|
|
```lux
|
|
fn main(): Unit with {Console, Random} = {
|
|
let n = Random.int(1, 100)
|
|
Console.print("Random number: " + toString(n))
|
|
}
|
|
|
|
let output = run main() with {}
|
|
```
|
|
|
|
## Effect Propagation
|
|
|
|
Effects propagate up the call stack:
|
|
|
|
```lux
|
|
fn helper(): Int with {Console} = {
|
|
Console.print("In helper")
|
|
42
|
|
}
|
|
|
|
// Must declare Console because it calls helper
|
|
fn caller(): Int with {Console} = {
|
|
let x = helper()
|
|
x * 2
|
|
}
|
|
|
|
// Error: caller uses Console but doesn't declare it
|
|
fn broken(): Int = {
|
|
caller() // Error!
|
|
}
|
|
```
|
|
|
|
The rule: if you call a function with effect E, you must either:
|
|
1. Declare E in your signature
|
|
2. Handle E with a `run ... with {}` block
|
|
|
|
## Running Effects
|
|
|
|
The `run ... with {}` block executes effectful code:
|
|
|
|
```lux
|
|
fn greet(): Unit with {Console} =
|
|
Console.print("Hello!")
|
|
|
|
// Execute with default handlers
|
|
let result = run greet() with {}
|
|
```
|
|
|
|
For built-in effects, `with {}` uses the default implementations (real console, real files, etc.).
|
|
|
|
## Custom Effects
|
|
|
|
You can define your own effects:
|
|
|
|
```lux
|
|
effect Logger {
|
|
fn log(level: String, message: String): Unit
|
|
fn getLevel(): String
|
|
}
|
|
```
|
|
|
|
This declares an effect with two operations. To use it:
|
|
|
|
```lux
|
|
fn processData(data: Int): Int with {Logger} = {
|
|
Logger.log("info", "Starting processing")
|
|
let result = data * 2
|
|
Logger.log("debug", "Result: " + toString(result))
|
|
result
|
|
}
|
|
```
|
|
|
|
But this won't run yet—we need a *handler*.
|
|
|
|
## Handlers
|
|
|
|
Handlers define how effect operations behave:
|
|
|
|
```lux
|
|
handler consoleLogger: Logger {
|
|
fn log(level, message) = {
|
|
Console.print("[" + level + "] " + message)
|
|
resume(())
|
|
}
|
|
fn getLevel() = resume("debug")
|
|
}
|
|
```
|
|
|
|
Key concept: **`resume(value)`** continues the computation with `value` as the result of the effect operation.
|
|
|
|
Now we can run:
|
|
|
|
```lux
|
|
fn main(): Unit with {Console} = {
|
|
let result = run processData(21) with {
|
|
Logger = consoleLogger
|
|
}
|
|
Console.print("Final: " + toString(result))
|
|
}
|
|
|
|
let output = run main() with {}
|
|
```
|
|
|
|
Output:
|
|
```
|
|
[info] Starting processing
|
|
[debug] Result: 42
|
|
Final: 42
|
|
```
|
|
|
|
## The Power of Handlers
|
|
|
|
### Different Implementations
|
|
|
|
Same code, different behaviors:
|
|
|
|
```lux
|
|
// Console logging
|
|
handler consoleLogger: Logger {
|
|
fn log(level, msg) = {
|
|
Console.print("[" + level + "] " + msg)
|
|
resume(())
|
|
}
|
|
fn getLevel() = resume("debug")
|
|
}
|
|
|
|
// Silent (for testing)
|
|
handler silentLogger: Logger {
|
|
fn log(level, msg) = resume(())
|
|
fn getLevel() = resume("none")
|
|
}
|
|
|
|
// Collecting logs
|
|
handler collectingLogger: Logger {
|
|
fn log(level, msg) = {
|
|
State.put(State.get() + "[" + level + "] " + msg + "\n")
|
|
resume(())
|
|
}
|
|
fn getLevel() = resume("all")
|
|
}
|
|
```
|
|
|
|
### Resumable Operations
|
|
|
|
Unlike exceptions, handlers can *continue* the computation:
|
|
|
|
```lux
|
|
effect Ask {
|
|
fn ask(prompt: String): String
|
|
}
|
|
|
|
fn survey(): String with {Ask} = {
|
|
let name = Ask.ask("Name?")
|
|
let age = Ask.ask("Age?")
|
|
name + " is " + age + " years old"
|
|
}
|
|
|
|
// Handler that provides answers
|
|
handler mockAnswers: Ask {
|
|
fn ask(prompt) =
|
|
if String.contains(prompt, "Name") then resume("Alice")
|
|
else resume("30")
|
|
}
|
|
|
|
run survey() with { Ask = mockAnswers }
|
|
// Returns: "Alice is 30 years old"
|
|
```
|
|
|
|
The computation pauses at `Ask.ask`, the handler provides a value, and `resume` continues from where it left off.
|
|
|
|
## Effect Composition
|
|
|
|
Multiple effects combine naturally:
|
|
|
|
```lux
|
|
fn program(): Int with {Console, Random, Logger} = {
|
|
Logger.log("info", "Starting")
|
|
let n = Random.int(1, 10)
|
|
Console.print("Got: " + toString(n))
|
|
Logger.log("debug", "Returning " + toString(n))
|
|
n
|
|
}
|
|
|
|
let result = run program() with {
|
|
Logger = consoleLogger
|
|
}
|
|
```
|
|
|
|
No monad transformers, no lifting, no complexity. Effects just work together.
|
|
|
|
## Why This Matters
|
|
|
|
### 1. Testability
|
|
|
|
```lux
|
|
// Production code
|
|
fn fetchUser(id: Int): String with {Http} =
|
|
Http.get("https://api.example.com/users/" + toString(id))
|
|
|
|
// Test with mock HTTP
|
|
handler mockHttp: Http {
|
|
fn get(url) = resume("{\"name\": \"Test User\"}")
|
|
// ... other operations
|
|
}
|
|
|
|
// Test runs without network
|
|
let result = run fetchUser(1) with { Http = mockHttp }
|
|
```
|
|
|
|
### 2. Explicit Dependencies
|
|
|
|
```lux
|
|
// You can see exactly what this function needs
|
|
fn processOrder(order: Order): Receipt with {Database, Logger, Email}
|
|
```
|
|
|
|
### 3. Controlled Side Effects
|
|
|
|
```lux
|
|
// This function is pure - it CAN'T do I/O
|
|
fn calculateTotal(items: List<Item>): Int =
|
|
List.fold(items, 0, fn(acc: Int, item: Item): Int => acc + item.price)
|
|
|
|
// Compile error if you try to add Console.print here
|
|
```
|
|
|
|
## Common Patterns
|
|
|
|
### Dependency Injection
|
|
|
|
```lux
|
|
effect Database {
|
|
fn query(sql: String): List<Row>
|
|
fn execute(sql: String): Int
|
|
}
|
|
|
|
fn getUsers(): List<User> with {Database} =
|
|
Database.query("SELECT * FROM users")
|
|
|
|
// Production
|
|
handler postgresDb: Database { /* real implementation */ }
|
|
|
|
// Testing
|
|
handler mockDb: Database {
|
|
fn query(sql) = resume([mockRow1, mockRow2])
|
|
fn execute(sql) = resume(1)
|
|
}
|
|
```
|
|
|
|
### Configuration
|
|
|
|
```lux
|
|
effect Config {
|
|
fn get(key: String): String
|
|
}
|
|
|
|
fn appUrl(): String with {Config} =
|
|
Config.get("APP_URL")
|
|
|
|
handler envConfig: Config {
|
|
fn get(key) = resume(Process.env(key))
|
|
}
|
|
|
|
handler testConfig: Config {
|
|
fn get(key) = resume("http://localhost:8080")
|
|
}
|
|
```
|
|
|
|
### Early Return
|
|
|
|
```lux
|
|
fn validateUser(user: User): User with {Fail} = {
|
|
if user.name == "" then Fail.fail("Name required")
|
|
else if user.age < 0 then Fail.fail("Invalid age")
|
|
else user
|
|
}
|
|
|
|
// Fail stops execution
|
|
let result = run validateUser(invalidUser) with {}
|
|
```
|
|
|
|
## Summary
|
|
|
|
| Concept | Syntax |
|
|
|---------|--------|
|
|
| Declare effect | `effect Name { fn op(): Type }` |
|
|
| Use effect | `fn f(): T with {Effect}` |
|
|
| Effect operation | `Effect.operation(args)` |
|
|
| Define handler | `handler name: Effect { fn op(...) = ... }` |
|
|
| Resume | `resume(value)` |
|
|
| Run with handler | `run expr with { Effect = handler }` |
|
|
|
|
## Next
|
|
|
|
[Chapter 6: Handlers](06-handlers.md) - Deep dive into handler patterns.
|