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>
252 lines
5.7 KiB
Markdown
252 lines
5.7 KiB
Markdown
# 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.
|