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:
380
docs/tutorials/dependency-injection.md
Normal file
380
docs/tutorials/dependency-injection.md
Normal file
@@ -0,0 +1,380 @@
|
||||
# Tutorial: Dependency Injection with Effects
|
||||
|
||||
Learn how effects provide natural dependency injection for testing and flexibility.
|
||||
|
||||
## What You'll Learn
|
||||
|
||||
- Using effects for dependencies
|
||||
- Swapping handlers for testing
|
||||
- The "ports and adapters" pattern
|
||||
|
||||
## The Problem
|
||||
|
||||
Imagine you're building a user registration system:
|
||||
|
||||
```javascript
|
||||
// Traditional approach (pseudo-code)
|
||||
function registerUser(userData) {
|
||||
const validated = validate(userData);
|
||||
database.insert(validated); // Hard-coded dependency
|
||||
emailService.send(validated.email, "Welcome!"); // Another one
|
||||
logger.info("User registered"); // And another
|
||||
return validated.id;
|
||||
}
|
||||
```
|
||||
|
||||
Testing this requires mocking `database`, `emailService`, and `logger`. In many languages, this means:
|
||||
- Dependency injection frameworks
|
||||
- Mock libraries
|
||||
- Complex test setup
|
||||
|
||||
## The Lux Way
|
||||
|
||||
In Lux, dependencies are effects:
|
||||
|
||||
```lux
|
||||
// Define what we need (not how it works)
|
||||
effect Database {
|
||||
fn insert(table: String, data: String): Int
|
||||
fn query(sql: String): List<String>
|
||||
}
|
||||
|
||||
effect Email {
|
||||
fn send(to: String, subject: String, body: String): Unit
|
||||
}
|
||||
|
||||
effect Logger {
|
||||
fn info(message: String): Unit
|
||||
fn error(message: String): Unit
|
||||
}
|
||||
```
|
||||
|
||||
Now our business logic declares its dependencies:
|
||||
|
||||
```lux
|
||||
type User = { name: String, email: String }
|
||||
|
||||
fn registerUser(user: User): Int with {Database, Email, Logger} = {
|
||||
Logger.info("Registering user: " + user.name)
|
||||
|
||||
// Validate
|
||||
if user.name == "" then {
|
||||
Logger.error("Empty name")
|
||||
Fail.fail("Name required")
|
||||
} else ()
|
||||
|
||||
if user.email == "" then {
|
||||
Logger.error("Empty email")
|
||||
Fail.fail("Email required")
|
||||
} else ()
|
||||
|
||||
// Insert into database
|
||||
let userId = Database.insert("users", userToJson(user))
|
||||
Logger.info("Created user with ID: " + toString(userId))
|
||||
|
||||
// Send welcome email
|
||||
Email.send(user.email, "Welcome!", "Thanks for registering, " + user.name)
|
||||
Logger.info("Sent welcome email")
|
||||
|
||||
userId
|
||||
}
|
||||
|
||||
fn userToJson(user: User): String =
|
||||
"{\"name\":\"" + user.name + "\",\"email\":\"" + user.email + "\"}"
|
||||
```
|
||||
|
||||
## Production Handlers
|
||||
|
||||
For production, we implement real handlers:
|
||||
|
||||
```lux
|
||||
handler postgresDb: Database {
|
||||
fn insert(table, data) = {
|
||||
let result = Http.post("http://db-service/insert", data)
|
||||
// Parse ID from response
|
||||
resume(parseId(result))
|
||||
}
|
||||
fn query(sql) = {
|
||||
let result = Http.post("http://db-service/query", sql)
|
||||
resume(parseRows(result))
|
||||
}
|
||||
}
|
||||
|
||||
handler smtpEmail: Email {
|
||||
fn send(to, subject, body) = {
|
||||
Http.post("http://email-service/send", formatEmail(to, subject, body))
|
||||
resume(())
|
||||
}
|
||||
}
|
||||
|
||||
handler consoleLogger: Logger {
|
||||
fn info(msg) = {
|
||||
Console.print("[INFO] " + msg)
|
||||
resume(())
|
||||
}
|
||||
fn error(msg) = {
|
||||
Console.print("[ERROR] " + msg)
|
||||
resume(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Production code:
|
||||
|
||||
```lux
|
||||
fn main(): Unit with {Console, Http} = {
|
||||
let user = User { name: "Alice", email: "alice@example.com" }
|
||||
|
||||
let result = run registerUser(user) with {
|
||||
Database = postgresDb,
|
||||
Email = smtpEmail,
|
||||
Logger = consoleLogger
|
||||
}
|
||||
|
||||
Console.print("Registered user ID: " + toString(result))
|
||||
}
|
||||
```
|
||||
|
||||
## Test Handlers
|
||||
|
||||
For testing, we swap in mock handlers:
|
||||
|
||||
```lux
|
||||
// Mock database that stores in memory
|
||||
handler mockDb: Database {
|
||||
fn insert(table, data) = {
|
||||
let id = State.get()
|
||||
State.put(id + 1)
|
||||
Console.print("[MOCK DB] Inserted into " + table + ": " + data)
|
||||
resume(id)
|
||||
}
|
||||
fn query(sql) = {
|
||||
Console.print("[MOCK DB] Query: " + sql)
|
||||
resume(["mock", "data"])
|
||||
}
|
||||
}
|
||||
|
||||
// Mock email that just logs
|
||||
handler mockEmail: Email {
|
||||
fn send(to, subject, body) = {
|
||||
Console.print("[MOCK EMAIL] To: " + to + ", Subject: " + subject)
|
||||
resume(())
|
||||
}
|
||||
}
|
||||
|
||||
// Silent logger for tests
|
||||
handler silentLogger: Logger {
|
||||
fn info(msg) = resume(())
|
||||
fn error(msg) = resume(())
|
||||
}
|
||||
|
||||
// Collecting logger for assertions
|
||||
handler collectingLogger: Logger {
|
||||
fn info(msg) = {
|
||||
State.put(State.get() + "[INFO] " + msg + "\n")
|
||||
resume(())
|
||||
}
|
||||
fn error(msg) = {
|
||||
State.put(State.get() + "[ERROR] " + msg + "\n")
|
||||
resume(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Test code:
|
||||
|
||||
```lux
|
||||
fn testRegisterUser(): Unit with {Console} = {
|
||||
let user = User { name: "Test", email: "test@test.com" }
|
||||
|
||||
// Run with mocks
|
||||
let (userId, logs) = run {
|
||||
let id = run {
|
||||
run registerUser(user) with {
|
||||
Database = mockDb,
|
||||
Email = mockEmail,
|
||||
Logger = collectingLogger
|
||||
}
|
||||
} with { State = 1 } // Database ID counter
|
||||
|
||||
let logs = State.get()
|
||||
(id, logs)
|
||||
} with { State = "" } // Log accumulator
|
||||
|
||||
// Assertions
|
||||
Console.print("User ID: " + toString(userId))
|
||||
Console.print("Logs:\n" + logs)
|
||||
|
||||
if userId == 1 then
|
||||
Console.print("✓ User ID is correct")
|
||||
else
|
||||
Console.print("✗ User ID is wrong")
|
||||
|
||||
if String.contains(logs, "Registering user") then
|
||||
Console.print("✓ Registration was logged")
|
||||
else
|
||||
Console.print("✗ Registration was not logged")
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Failure Cases
|
||||
|
||||
Test error handling by using handlers that fail:
|
||||
|
||||
```lux
|
||||
handler failingDb: Database {
|
||||
fn insert(table, data) = {
|
||||
Fail.fail("Database connection failed")
|
||||
}
|
||||
fn query(sql) = resume([])
|
||||
}
|
||||
|
||||
fn testDatabaseFailure(): Unit with {Console} = {
|
||||
let user = User { name: "Test", email: "test@test.com" }
|
||||
|
||||
let result = run {
|
||||
run registerUser(user) with {
|
||||
Database = failingDb,
|
||||
Email = mockEmail,
|
||||
Logger = silentLogger
|
||||
}
|
||||
} with {}
|
||||
|
||||
Console.print("Test: Database failure is handled")
|
||||
// The Fail effect should have triggered
|
||||
}
|
||||
```
|
||||
|
||||
## The "Ports and Adapters" Pattern
|
||||
|
||||
This is also known as hexagonal architecture:
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ Business Logic │
|
||||
│ (Pure + Effects) │
|
||||
└─────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
│ │ │
|
||||
┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐
|
||||
│ Database │ │ Email │ │ Logger │
|
||||
│ Effect │ │ Effect │ │ Effect │
|
||||
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
|
||||
│ │ │
|
||||
┌─────────┼─────────┐ │ ┌─────┼─────┐
|
||||
│ │ │ │ │ │
|
||||
┌───┴───┐ ┌───┴───┐ │ ┌───┴───┐ ┌───┴───┐ ┌─────┴─────┐
|
||||
│Postgres│ │MockDB │ │ │ SMTP │ │Console│ │ Collector │
|
||||
└───────┘ └───────┘ │ └───────┘ └───────┘ └───────────┘
|
||||
│
|
||||
┌─────┴─────┐
|
||||
│ MockEmail │
|
||||
└───────────┘
|
||||
```
|
||||
|
||||
- **Business logic** is pure (plus declared effects)
|
||||
- **Effects** are the ports (interfaces)
|
||||
- **Handlers** are the adapters (implementations)
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **No mocking libraries needed** - Just write a different handler
|
||||
2. **Type-safe** - The compiler ensures all effects are handled
|
||||
3. **Explicit dependencies** - You can see what a function needs
|
||||
4. **Easy to test** - Swap handlers, no reflection or magic
|
||||
5. **Flexible** - Same code, different environments
|
||||
|
||||
## Complete Example
|
||||
|
||||
```lux
|
||||
// main.lux
|
||||
|
||||
// === Effects (Ports) ===
|
||||
|
||||
effect Database {
|
||||
fn insert(table: String, data: String): Int
|
||||
}
|
||||
|
||||
effect Email {
|
||||
fn send(to: String, body: String): Unit
|
||||
}
|
||||
|
||||
// === Business Logic ===
|
||||
|
||||
type User = { name: String, email: String }
|
||||
|
||||
fn registerUser(user: User): Int with {Database, Email, Fail} = {
|
||||
if user.name == "" then Fail.fail("Name required") else ()
|
||||
let id = Database.insert("users", user.name)
|
||||
Email.send(user.email, "Welcome!")
|
||||
id
|
||||
}
|
||||
|
||||
// === Production Handlers (Adapters) ===
|
||||
|
||||
handler prodDb: Database {
|
||||
fn insert(table, data) = {
|
||||
Console.print("[DB] INSERT INTO " + table)
|
||||
resume(42) // Would be real ID
|
||||
}
|
||||
}
|
||||
|
||||
handler prodEmail: Email {
|
||||
fn send(to, body) = {
|
||||
Console.print("[EMAIL] Sending to " + to)
|
||||
resume(())
|
||||
}
|
||||
}
|
||||
|
||||
// === Test Handlers ===
|
||||
|
||||
handler testDb: Database {
|
||||
fn insert(table, data) = resume(1)
|
||||
}
|
||||
|
||||
handler testEmail: Email {
|
||||
fn send(to, body) = resume(())
|
||||
}
|
||||
|
||||
// === Running ===
|
||||
|
||||
fn production(): Unit with {Console} = {
|
||||
let user = User { name: "Alice", email: "alice@example.com" }
|
||||
let id = run registerUser(user) with {
|
||||
Database = prodDb,
|
||||
Email = prodEmail
|
||||
}
|
||||
Console.print("Created user: " + toString(id))
|
||||
}
|
||||
|
||||
fn test(): Unit with {Console} = {
|
||||
let user = User { name: "Test", email: "test@test.com" }
|
||||
let id = run registerUser(user) with {
|
||||
Database = testDb,
|
||||
Email = testEmail
|
||||
}
|
||||
if id == 1 then Console.print("✓ Test passed")
|
||||
else Console.print("✗ Test failed")
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== Production ===")
|
||||
production()
|
||||
Console.print("\n=== Test ===")
|
||||
test()
|
||||
}
|
||||
|
||||
let output = run main() with {}
|
||||
```
|
||||
|
||||
## What You Learned
|
||||
|
||||
- Effects define **what** you need, handlers define **how**
|
||||
- Swap handlers for testing without changing business logic
|
||||
- No dependency injection framework needed
|
||||
- Type system ensures all dependencies are satisfied
|
||||
|
||||
## Next Tutorial
|
||||
|
||||
[State Machines](state-machines.md) - Model state transitions with custom effects.
|
||||
Reference in New Issue
Block a user