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:
2026-02-13 17:43:41 -05:00
parent 9ee7148d24
commit 44f88afcf8
16 changed files with 4845 additions and 0 deletions

View 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.