Files
lux/docs/guide/05-effects.md
Brandon Lucas 7e76acab18 feat: rebuild website with full learning funnel
Website rebuilt from scratch based on analysis of 11 beloved language
websites (Elm, Zig, Gleam, Swift, Kotlin, Haskell, OCaml, Crystal, Roc,
Rust, Go).

New website structure:
- Homepage with hero, playground, three pillars, install guide
- Language Tour with interactive lessons (hello world, types, effects)
- Examples cookbook with categorized sidebar
- API documentation index
- Installation guide (Nix and source)
- Sleek/noble design (black/gold, serif typography)

Also includes:
- New stdlib/json.lux module for JSON serialization
- Enhanced stdlib/http.lux with middleware and routing
- New string functions (charAt, indexOf, lastIndexOf, repeat)
- LSP improvements (rename, signature help, formatting)
- Package manager transitive dependency resolution
- Updated documentation for effects and stdlib
- New showcase example (task_manager.lux)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-16 23:05:35 -05:00

358 lines
8.0 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 |
| `Time` | `now`, `sleep` | Time operations |
| `File` | `read`, `write`, `exists`, `delete`, `list`, `mkdir` | File system |
| `Process` | `exec`, `env`, `args`, `cwd`, `exit` | System processes |
| `Http` | `get`, `post`, `put`, `delete` | HTTP client |
| `HttpServer` | `listen`, `accept`, `respond`, `stop` | HTTP server |
| `Sql` | `open`, `openMemory`, `close`, `execute`, `query`, `queryOne`, `beginTx`, `commit`, `rollback` | SQLite database |
| `Postgres` | `connect`, `close`, `execute`, `query`, `queryOne` | PostgreSQL database |
| `Concurrent` | `spawn`, `await`, `yield`, `sleep`, `cancel`, `isRunning`, `taskCount` | Concurrent tasks |
| `Channel` | `create`, `send`, `receive`, `tryReceive`, `close` | Inter-task communication |
| `Test` | `assert`, `assertEqual`, `assertTrue`, `assertFalse` | Testing |
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.