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:
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.
|
||||
Reference in New Issue
Block a user