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:
350
docs/guide/05-effects.md
Normal file
350
docs/guide/05-effects.md
Normal file
@@ -0,0 +1,350 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user