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:
125
docs/tutorials/README.md
Normal file
125
docs/tutorials/README.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Tutorials
|
||||
|
||||
Learn Lux by building real projects.
|
||||
|
||||
## Standard Programs
|
||||
|
||||
These tutorials cover common programming tasks:
|
||||
|
||||
| Tutorial | What You'll Build | Concepts |
|
||||
|----------|-------------------|----------|
|
||||
| [Calculator](calculator.md) | REPL calculator | Parsing, evaluation, REPL loop |
|
||||
| [Todo App](todo.md) | CLI task manager | File I/O, data structures |
|
||||
| [HTTP Client](http-client.md) | API consumer | HTTP effects, JSON parsing |
|
||||
| [Word Counter](word-counter.md) | Text analyzer | File reading, string ops |
|
||||
|
||||
## Effect Showcases
|
||||
|
||||
These tutorials demonstrate Lux's unique effect system:
|
||||
|
||||
| Tutorial | What You'll Learn | Key Concept |
|
||||
|----------|-------------------|-------------|
|
||||
| [Dependency Injection](dependency-injection.md) | Testing with mock handlers | Handler swapping |
|
||||
| [State Machines](state-machines.md) | Modeling state transitions | Custom effects |
|
||||
| [Effects Cookbook](effects-cookbook.md) | Common effect patterns | Handler patterns |
|
||||
|
||||
## Quick Start: Your First Project
|
||||
|
||||
### 1. Create Project Directory
|
||||
|
||||
```bash
|
||||
mkdir my-first-lux
|
||||
cd my-first-lux
|
||||
```
|
||||
|
||||
### 2. Create Main File
|
||||
|
||||
```lux
|
||||
// main.lux
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("Welcome to my Lux project!")
|
||||
Console.print("Enter your name:")
|
||||
let name = Console.readLine()
|
||||
Console.print("Hello, " + name + "!")
|
||||
}
|
||||
|
||||
let output = run main() with {}
|
||||
```
|
||||
|
||||
### 3. Run It
|
||||
|
||||
```bash
|
||||
lux main.lux
|
||||
```
|
||||
|
||||
### 4. Add a Module
|
||||
|
||||
```lux
|
||||
// lib/greetings.lux
|
||||
|
||||
pub fn hello(name: String): String =
|
||||
"Hello, " + name + "!"
|
||||
|
||||
pub fn goodbye(name: String): String =
|
||||
"Goodbye, " + name + "!"
|
||||
```
|
||||
|
||||
```lux
|
||||
// main.lux
|
||||
import lib/greetings as greet
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("Enter your name:")
|
||||
let name = Console.readLine()
|
||||
Console.print(greet.hello(name))
|
||||
Console.print(greet.goodbye(name))
|
||||
}
|
||||
|
||||
let output = run main() with {}
|
||||
```
|
||||
|
||||
## Project Ideas by Difficulty
|
||||
|
||||
### Beginner
|
||||
|
||||
- **Temperature converter** - Convert between Celsius, Fahrenheit, Kelvin
|
||||
- **Number guessing game** - Random number with hints
|
||||
- **Simple quiz** - Multiple choice questions with scoring
|
||||
- **Unit converter** - Length, weight, volume conversions
|
||||
|
||||
### Intermediate
|
||||
|
||||
- **Markdown previewer** - Parse basic Markdown to HTML
|
||||
- **Contact book** - CRUD with file persistence
|
||||
- **Simple grep** - Search files for patterns
|
||||
- **CSV processor** - Read, filter, transform CSV files
|
||||
|
||||
### Advanced
|
||||
|
||||
- **Test framework** - Use effects for test isolation
|
||||
- **Config loader** - Effect-based configuration with validation
|
||||
- **Mini interpreter** - Build a small language
|
||||
- **Chat client** - HTTP-based chat application
|
||||
|
||||
### Effect Showcases
|
||||
|
||||
- **Transaction system** - Rollback on failure
|
||||
- **Capability security** - Effects as capabilities
|
||||
- **Async simulation** - Model async with effects
|
||||
- **Dependency graph** - Track and inject dependencies
|
||||
|
||||
## How to Use These Tutorials
|
||||
|
||||
1. **Read through first** - Understand the goal
|
||||
2. **Type the code** - Don't copy-paste
|
||||
3. **Experiment** - Modify and see what happens
|
||||
4. **Build your own** - Apply concepts to your ideas
|
||||
|
||||
## Getting Help
|
||||
|
||||
- Check the [Language Reference](../reference/syntax.md)
|
||||
- See [examples/](../../examples/) for working code
|
||||
- Use the REPL to experiment
|
||||
|
||||
Happy building!
|
||||
251
docs/tutorials/calculator.md
Normal file
251
docs/tutorials/calculator.md
Normal file
@@ -0,0 +1,251 @@
|
||||
# Tutorial: Building a Calculator
|
||||
|
||||
Build a REPL calculator that evaluates arithmetic expressions.
|
||||
|
||||
## What You'll Learn
|
||||
|
||||
- Defining algebraic data types
|
||||
- Pattern matching
|
||||
- Recursive functions
|
||||
- REPL loops with effects
|
||||
|
||||
## The Goal
|
||||
|
||||
```
|
||||
Calculator REPL
|
||||
> 2 + 3
|
||||
5
|
||||
> (10 - 4) * 2
|
||||
12
|
||||
> 100 / 5 + 3
|
||||
23
|
||||
> quit
|
||||
Goodbye!
|
||||
```
|
||||
|
||||
## Step 1: Define the Expression Type
|
||||
|
||||
First, we model arithmetic expressions:
|
||||
|
||||
```lux
|
||||
// calculator.lux
|
||||
|
||||
type Expr =
|
||||
| Num(Int)
|
||||
| Add(Expr, Expr)
|
||||
| Sub(Expr, Expr)
|
||||
| Mul(Expr, Expr)
|
||||
| Div(Expr, Expr)
|
||||
```
|
||||
|
||||
This is an *algebraic data type* (ADT). An `Expr` is one of:
|
||||
- A number: `Num(42)`
|
||||
- An addition: `Add(Num(2), Num(3))`
|
||||
- And so on...
|
||||
|
||||
## Step 2: Evaluate Expressions
|
||||
|
||||
Now we evaluate expressions to integers:
|
||||
|
||||
```lux
|
||||
fn eval(e: Expr): Result<Int, String> =
|
||||
match e {
|
||||
Num(n) => Ok(n),
|
||||
Add(a, b) => evalBinOp(a, b, fn(x: Int, y: Int): Int => x + y),
|
||||
Sub(a, b) => evalBinOp(a, b, fn(x: Int, y: Int): Int => x - y),
|
||||
Mul(a, b) => evalBinOp(a, b, fn(x: Int, y: Int): Int => x * y),
|
||||
Div(a, b) => {
|
||||
match (eval(a), eval(b)) {
|
||||
(Ok(x), Ok(0)) => Err("Division by zero"),
|
||||
(Ok(x), Ok(y)) => Ok(x / y),
|
||||
(Err(e), _) => Err(e),
|
||||
(_, Err(e)) => Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn evalBinOp(a: Expr, b: Expr, op: fn(Int, Int): Int): Result<Int, String> =
|
||||
match (eval(a), eval(b)) {
|
||||
(Ok(x), Ok(y)) => Ok(op(x, y)),
|
||||
(Err(e), _) => Err(e),
|
||||
(_, Err(e)) => Err(e)
|
||||
}
|
||||
```
|
||||
|
||||
Pattern matching makes this clear:
|
||||
- `Num(n)` just returns the number
|
||||
- Operations evaluate both sides, then apply the operator
|
||||
- Division checks for zero
|
||||
|
||||
## Step 3: Parse Expressions
|
||||
|
||||
For simplicity, we'll parse a limited format. A real parser would be more complex.
|
||||
|
||||
```lux
|
||||
fn parseSimple(input: String): Result<Expr, String> = {
|
||||
let trimmed = String.trim(input)
|
||||
|
||||
// Try to parse as a number
|
||||
if isNumber(trimmed) then
|
||||
Ok(Num(parseInt(trimmed)))
|
||||
else
|
||||
// Try to find an operator
|
||||
match findOperator(trimmed) {
|
||||
Some((left, op, right)) => {
|
||||
match (parseSimple(left), parseSimple(right)) {
|
||||
(Ok(l), Ok(r)) => Ok(makeOp(l, op, r)),
|
||||
(Err(e), _) => Err(e),
|
||||
(_, Err(e)) => Err(e)
|
||||
}
|
||||
},
|
||||
None => Err("Cannot parse: " + trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
fn isNumber(s: String): Bool = {
|
||||
let chars = String.chars(s)
|
||||
List.all(chars, fn(c: String): Bool =>
|
||||
c == "-" || (c >= "0" && c <= "9")
|
||||
)
|
||||
}
|
||||
|
||||
fn parseInt(s: String): Int = {
|
||||
// Simplified - assumes valid integer
|
||||
List.fold(String.chars(s), 0, fn(acc: Int, c: String): Int =>
|
||||
if c == "-" then acc
|
||||
else acc * 10 + charToDigit(c)
|
||||
) * (if String.startsWith(s, "-") then -1 else 1)
|
||||
}
|
||||
|
||||
fn charToDigit(c: String): Int =
|
||||
match c {
|
||||
"0" => 0, "1" => 1, "2" => 2, "3" => 3, "4" => 4,
|
||||
"5" => 5, "6" => 6, "7" => 7, "8" => 8, "9" => 9,
|
||||
_ => 0
|
||||
}
|
||||
|
||||
fn findOperator(s: String): Option<(String, String, String)> = {
|
||||
// Find last +/- at depth 0 (lowest precedence)
|
||||
// Then *// (higher precedence)
|
||||
// This is a simplified approach
|
||||
let addSub = findOpAtDepth(s, ["+", "-"], 0)
|
||||
match addSub {
|
||||
Some(r) => Some(r),
|
||||
None => findOpAtDepth(s, ["*", "/"], 0)
|
||||
}
|
||||
}
|
||||
|
||||
fn makeOp(left: Expr, op: String, right: Expr): Expr =
|
||||
match op {
|
||||
"+" => Add(left, right),
|
||||
"-" => Sub(left, right),
|
||||
"*" => Mul(left, right),
|
||||
"/" => Div(left, right),
|
||||
_ => Num(0) // Should not happen
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4: The REPL Loop
|
||||
|
||||
Now we build the interactive loop:
|
||||
|
||||
```lux
|
||||
fn repl(): Unit with {Console} = {
|
||||
Console.print("Calculator REPL (type 'quit' to exit)")
|
||||
replLoop()
|
||||
}
|
||||
|
||||
fn replLoop(): Unit with {Console} = {
|
||||
Console.print("> ")
|
||||
let input = Console.readLine()
|
||||
|
||||
if input == "quit" then
|
||||
Console.print("Goodbye!")
|
||||
else {
|
||||
match parseSimple(input) {
|
||||
Ok(expr) => {
|
||||
match eval(expr) {
|
||||
Ok(result) => Console.print(toString(result)),
|
||||
Err(e) => Console.print("Error: " + e)
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("Parse error: " + e)
|
||||
}
|
||||
replLoop() // Continue the loop
|
||||
}
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = repl()
|
||||
|
||||
let output = run main() with {}
|
||||
```
|
||||
|
||||
## Step 5: Test It
|
||||
|
||||
```bash
|
||||
$ lux calculator.lux
|
||||
Calculator REPL (type 'quit' to exit)
|
||||
> 2 + 3
|
||||
5
|
||||
> 10 - 4
|
||||
6
|
||||
> 6 * 7
|
||||
42
|
||||
> 100 / 0
|
||||
Error: Division by zero
|
||||
> quit
|
||||
Goodbye!
|
||||
```
|
||||
|
||||
## Extending the Calculator
|
||||
|
||||
Try adding:
|
||||
|
||||
1. **Parentheses**: Parse `(2 + 3) * 4`
|
||||
2. **Variables**: Store results in named variables
|
||||
3. **Functions**: `sqrt`, `abs`, `pow`
|
||||
4. **History**: Use State effect to track previous results
|
||||
|
||||
### Adding Variables (Bonus)
|
||||
|
||||
```lux
|
||||
effect Variables {
|
||||
fn get(name: String): Option<Int>
|
||||
fn set(name: String, value: Int): Unit
|
||||
}
|
||||
|
||||
fn evalWithVars(e: Expr): Result<Int, String> with {Variables} =
|
||||
match e {
|
||||
Var(name) => {
|
||||
match Variables.get(name) {
|
||||
Some(v) => Ok(v),
|
||||
None => Err("Unknown variable: " + name)
|
||||
}
|
||||
},
|
||||
// ... other cases
|
||||
}
|
||||
|
||||
handler memoryVars: Variables {
|
||||
fn get(name) = resume(State.get(name))
|
||||
fn set(name, value) = {
|
||||
State.put(name, value)
|
||||
resume(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Code
|
||||
|
||||
See `examples/tutorials/calculator.lux` for the full implementation.
|
||||
|
||||
## What You Learned
|
||||
|
||||
- **ADTs** model structured data
|
||||
- **Pattern matching** destructures data cleanly
|
||||
- **Recursion** processes nested structures
|
||||
- **Result** handles errors without exceptions
|
||||
- **REPL loops** combine effects naturally
|
||||
|
||||
## Next Tutorial
|
||||
|
||||
[Todo App](todo.md) - Build a task manager with file persistence.
|
||||
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.
|
||||
325
docs/tutorials/project-ideas.md
Normal file
325
docs/tutorials/project-ideas.md
Normal file
@@ -0,0 +1,325 @@
|
||||
# Project Ideas
|
||||
|
||||
Here are projects to build with Lux, organized by difficulty and purpose.
|
||||
|
||||
## Beginner Projects
|
||||
|
||||
### 1. Temperature Converter
|
||||
Convert between Celsius, Fahrenheit, and Kelvin.
|
||||
|
||||
**Skills**: Basic I/O, functions, conditionals
|
||||
|
||||
```lux
|
||||
// Starter code
|
||||
fn celsiusToFahrenheit(c: Float): Float = c * 9.0 / 5.0 + 32.0
|
||||
fn fahrenheitToCelsius(f: Float): Float = (f - 32.0) * 5.0 / 9.0
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("Temperature Converter")
|
||||
Console.print("1. Celsius to Fahrenheit")
|
||||
Console.print("2. Fahrenheit to Celsius")
|
||||
// ... implement menu and conversion
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Number Guessing Game
|
||||
Computer picks a number, user guesses with hints.
|
||||
|
||||
**Skills**: Random effect, loops, conditionals
|
||||
|
||||
```lux
|
||||
fn game(): Unit with {Console, Random} = {
|
||||
let secret = Random.int(1, 100)
|
||||
Console.print("I'm thinking of a number 1-100...")
|
||||
guessLoop(secret, 1)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Word Counter
|
||||
Count words, lines, and characters in a file.
|
||||
|
||||
**Skills**: File effect, string operations
|
||||
|
||||
```lux
|
||||
fn countFile(path: String): Unit with {Console, File} = {
|
||||
let content = File.read(path)
|
||||
let lines = String.lines(content)
|
||||
let words = countWords(content)
|
||||
let chars = String.length(content)
|
||||
// ... display results
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Simple Quiz
|
||||
Multiple choice questions with scoring.
|
||||
|
||||
**Skills**: ADTs, pattern matching, state
|
||||
|
||||
```lux
|
||||
type Question = { text: String, options: List<String>, correct: Int }
|
||||
|
||||
fn askQuestion(q: Question): Bool with {Console} = {
|
||||
Console.print(q.text)
|
||||
// ... display options and check answer
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Intermediate Projects
|
||||
|
||||
### 5. Contact Book
|
||||
CRUD operations with file persistence.
|
||||
|
||||
**Skills**: File I/O, JSON, ADTs, effects
|
||||
|
||||
```lux
|
||||
type Contact = { name: String, email: String, phone: String }
|
||||
|
||||
effect ContactStore {
|
||||
fn add(contact: Contact): Int
|
||||
fn find(name: String): Option<Contact>
|
||||
fn list(): List<Contact>
|
||||
fn delete(id: Int): Bool
|
||||
}
|
||||
|
||||
// Implement handlers for file-based and in-memory storage
|
||||
```
|
||||
|
||||
### 6. Markdown Parser
|
||||
Parse basic Markdown to HTML.
|
||||
|
||||
**Skills**: Parsing, string manipulation, ADTs
|
||||
|
||||
```lux
|
||||
type MarkdownNode =
|
||||
| Heading(Int, String) // level, text
|
||||
| Paragraph(String)
|
||||
| Bold(String)
|
||||
| Italic(String)
|
||||
| Code(String)
|
||||
| List(List<String>)
|
||||
| Link(String, String) // text, url
|
||||
|
||||
fn parseMarkdown(input: String): List<MarkdownNode> = ...
|
||||
fn toHtml(nodes: List<MarkdownNode>): String = ...
|
||||
```
|
||||
|
||||
### 7. Simple HTTP API Client
|
||||
Fetch and display data from a REST API.
|
||||
|
||||
**Skills**: HTTP effect, JSON parsing
|
||||
|
||||
```lux
|
||||
fn fetchWeather(city: String): Unit with {Console, Http} = {
|
||||
let response = Http.get("https://api.weather.com/city/" + city)
|
||||
let data = Json.parse(response)
|
||||
let temp = Json.getFloat(data, "temperature")
|
||||
Console.print(city + ": " + toString(temp) + "°C")
|
||||
}
|
||||
```
|
||||
|
||||
### 8. File Backup Tool
|
||||
Copy files with logging and error handling.
|
||||
|
||||
**Skills**: File effect, error handling, recursion
|
||||
|
||||
```lux
|
||||
fn backup(source: String, dest: String): Unit with {File, Console, Fail} = {
|
||||
if File.isDirectory(source) then
|
||||
backupDirectory(source, dest)
|
||||
else
|
||||
backupFile(source, dest)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Projects
|
||||
|
||||
### 9. Effect-Based Test Framework
|
||||
Use effects for test isolation and assertions.
|
||||
|
||||
**Skills**: Custom effects, handlers, composition
|
||||
|
||||
```lux
|
||||
effect Assert {
|
||||
fn equal<T>(actual: T, expected: T): Unit
|
||||
fn true(condition: Bool): Unit
|
||||
fn fail(message: String): Unit
|
||||
}
|
||||
|
||||
effect Test {
|
||||
fn describe(name: String, tests: fn(): Unit): Unit
|
||||
fn it(name: String, test: fn(): Unit): Unit
|
||||
}
|
||||
|
||||
// Handlers collect results, run tests, report
|
||||
```
|
||||
|
||||
### 10. Configuration DSL
|
||||
Type-safe configuration with validation.
|
||||
|
||||
**Skills**: Effects, validation, ADTs
|
||||
|
||||
```lux
|
||||
effect Config {
|
||||
fn required(key: String): String
|
||||
fn optional(key: String, default: String): String
|
||||
fn validate(key: String, validator: fn(String): Bool): String
|
||||
}
|
||||
|
||||
fn loadAppConfig(): AppConfig with {Config, Fail} = {
|
||||
AppConfig {
|
||||
host: Config.required("HOST"),
|
||||
port: Config.validate("PORT", isValidPort),
|
||||
debug: Config.optional("DEBUG", "false") == "true"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 11. Mini Language Interpreter
|
||||
Build an interpreter for a simple language.
|
||||
|
||||
**Skills**: Parsing, ADTs, recursion, effects
|
||||
|
||||
```lux
|
||||
type Expr = ...
|
||||
type Stmt = ...
|
||||
type Value = ...
|
||||
|
||||
effect Runtime {
|
||||
fn getVar(name: String): Value
|
||||
fn setVar(name: String, value: Value): Unit
|
||||
fn print(value: Value): Unit
|
||||
}
|
||||
|
||||
fn interpret(program: List<Stmt>): Unit with {Runtime} = ...
|
||||
```
|
||||
|
||||
### 12. Task Scheduler
|
||||
Schedule and run tasks with dependencies.
|
||||
|
||||
**Skills**: Graphs, effects, async simulation
|
||||
|
||||
```lux
|
||||
type Task = { id: String, deps: List<String>, action: fn(): Unit }
|
||||
|
||||
effect Scheduler {
|
||||
fn schedule(task: Task): Unit
|
||||
fn run(): Unit
|
||||
fn wait(taskId: String): Unit
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Effect Showcase Projects
|
||||
|
||||
These projects specifically highlight Lux's effect system.
|
||||
|
||||
### 13. Transactional Operations
|
||||
Rollback on failure using effects.
|
||||
|
||||
```lux
|
||||
effect Transaction {
|
||||
fn begin(): Unit
|
||||
fn commit(): Unit
|
||||
fn rollback(): Unit
|
||||
}
|
||||
|
||||
fn transfer(from: Account, to: Account, amount: Int): Unit
|
||||
with {Transaction, Database, Fail} = {
|
||||
Transaction.begin()
|
||||
Database.debit(from, amount)
|
||||
Database.credit(to, amount) // If this fails, rollback
|
||||
Transaction.commit()
|
||||
}
|
||||
```
|
||||
|
||||
### 14. Mock HTTP for Testing
|
||||
Swap real HTTP with recorded responses.
|
||||
|
||||
```lux
|
||||
handler recordedHttp(responses: Map<String, String>): Http {
|
||||
fn get(url) = {
|
||||
match Map.get(responses, url) {
|
||||
Some(body) => resume(body),
|
||||
None => Fail.fail("No recorded response for: " + url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test with recorded responses
|
||||
let testResponses = Map.from([
|
||||
("https://api.example.com/users", "[{\"id\": 1}]")
|
||||
])
|
||||
|
||||
run fetchUsers() with { Http = recordedHttp(testResponses) }
|
||||
```
|
||||
|
||||
### 15. Capability-Based Security
|
||||
Use effects as capabilities.
|
||||
|
||||
```lux
|
||||
effect FileRead { fn read(path: String): String }
|
||||
effect FileWrite { fn write(path: String, content: String): Unit }
|
||||
effect Network { fn fetch(url: String): String }
|
||||
|
||||
// This function can ONLY read files - it cannot write or use network
|
||||
fn processConfig(path: String): Config with {FileRead} = ...
|
||||
|
||||
// This function has network but no file access
|
||||
fn fetchData(url: String): Data with {Network} = ...
|
||||
```
|
||||
|
||||
### 16. Async Simulation
|
||||
Model async operations with effects.
|
||||
|
||||
```lux
|
||||
effect Async {
|
||||
fn spawn(task: fn(): T): Future<T>
|
||||
fn await(future: Future<T>): T
|
||||
fn sleep(ms: Int): Unit
|
||||
}
|
||||
|
||||
fn parallel(): List<Int> with {Async} = {
|
||||
let f1 = Async.spawn(fn(): Int => compute1())
|
||||
let f2 = Async.spawn(fn(): Int => compute2())
|
||||
let f3 = Async.spawn(fn(): Int => compute3())
|
||||
[Async.await(f1), Async.await(f2), Async.await(f3)]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Complexity Guide
|
||||
|
||||
| Project | Effects Used | Lines of Code | Time |
|
||||
|---------|--------------|---------------|------|
|
||||
| Temperature Converter | Console | ~50 | 1 hour |
|
||||
| Guessing Game | Console, Random | ~80 | 2 hours |
|
||||
| Word Counter | Console, File | ~60 | 1 hour |
|
||||
| Contact Book | Console, File, Custom | ~200 | 4 hours |
|
||||
| Markdown Parser | Pure + Console | ~300 | 6 hours |
|
||||
| Test Framework | Custom effects | ~400 | 8 hours |
|
||||
| Mini Interpreter | Custom effects | ~600 | 16 hours |
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. **Pick a project** at your skill level
|
||||
2. **Break it down** into smaller tasks
|
||||
3. **Start with types** - define your data structures
|
||||
4. **Add effects** - what I/O do you need?
|
||||
5. **Implement logic** - write pure functions first
|
||||
6. **Test with handlers** - swap in mock handlers
|
||||
|
||||
## Need Help?
|
||||
|
||||
- Check `examples/` for working code
|
||||
- Read the [Effects Guide](../guide/05-effects.md)
|
||||
- Experiment in the REPL
|
||||
|
||||
Happy building!
|
||||
Reference in New Issue
Block a user