Files
lux/docs/tutorials/calculator.md
Brandon Lucas 44f88afcf8 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>
2026-02-13 17:43:41 -05:00

5.7 KiB

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:

// 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:

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.

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:

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

$ 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)

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 - Build a task manager with file persistence.