diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..e233b55 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,227 @@ +# Lux Documentation + +**Lux** is a functional programming language with first-class algebraic effects. It combines the safety of static typing with the flexibility of effect handlers, letting you write pure code that can do impure things—without losing control. + +## Quick Links + +| I want to... | Go to... | +|--------------|----------| +| Try Lux in 5 minutes | [Quick Start](#quick-start) | +| Learn Lux systematically | [The Lux Guide](guide/01-introduction.md) | +| Understand effects | [Effects Guide](guide/05-effects.md) | +| Look up syntax | [Language Reference](reference/syntax.md) | +| See the standard library | [Standard Library](reference/stdlib.md) | +| Build something | [Tutorials](tutorials/README.md) | + +--- + +## Quick Start + +### Install + +```bash +# Clone and build +git clone https://github.com/your-org/lux +cd lux +cargo build --release + +# Or with Nix +nix build +``` + +### Hello World + +Create `hello.lux`: + +```lux +fn main(): Unit with {Console} = + Console.print("Hello, World!") + +let output = run main() with {} +``` + +Run it: + +```bash +lux hello.lux +# Output: Hello, World! +``` + +### The REPL + +```bash +$ lux +Lux v0.1.0 - Type :help for commands + +> 1 + 2 * 3 +7 + +> fn square(x: Int): Int = x * x + + +> square(5) +25 + +> List.map([1, 2, 3], fn(x: Int): Int => x * 2) +[2, 4, 6] +``` + +--- + +## What Makes Lux Different? + +### 1. Effects Are Explicit + +In most languages, a function can do anything—print, crash, launch missiles—and the type doesn't tell you. In Lux, side effects are tracked: + +```lux +// This function ONLY does math—the type guarantees it +fn add(a: Int, b: Int): Int = a + b + +// This function uses Console—you can see it in the signature +fn greet(name: String): Unit with {Console} = + Console.print("Hello, " + name) +``` + +### 2. Effects Are Handleable + +Effects aren't just tracked—they're *values* you can intercept and redefine: + +```lux +effect Logger { + fn log(msg: String): Unit +} + +fn compute(): Int with {Logger} = { + Logger.log("Starting") + let result = 42 + Logger.log("Done") + result +} + +// Run with console logging +handler consoleLogger: Logger { + fn log(msg) = { + Console.print("[LOG] " + msg) + resume(()) + } +} + +let result = run compute() with { Logger = consoleLogger } +``` + +### 3. Handlers Can Resume + +Unlike exceptions, effect handlers can *continue* the computation: + +```lux +effect Ask { + fn ask(question: String): String +} + +fn survey(): Unit with {Ask, Console} = { + let name = Ask.ask("What's your name?") + let color = Ask.ask("Favorite color?") + Console.print(name + " likes " + color) +} + +// Handler that provides answers +handler autoAnswer: Ask { + fn ask(q) = + if String.contains(q, "name") then resume("Alice") + else resume("blue") +} + +run survey() with { Ask = autoAnswer } +// Output: Alice likes blue +``` + +--- + +## Documentation Structure + +### [The Lux Guide](guide/01-introduction.md) +A sequential guide to learning Lux, from basics to advanced topics. + +1. [Introduction](guide/01-introduction.md) - Why Lux, installation, first program +2. [Basic Types](guide/02-basic-types.md) - Int, String, Bool, functions +3. [Functions](guide/03-functions.md) - Definitions, closures, higher-order +4. [Data Types](guide/04-data-types.md) - ADTs, pattern matching, records +5. [Effects](guide/05-effects.md) - The core innovation +6. [Handlers](guide/06-handlers.md) - Defining and using handlers +7. [Modules](guide/07-modules.md) - Code organization, imports +8. [Error Handling](guide/08-errors.md) - Fail effect, Option, Result +9. [Standard Library](guide/09-stdlib.md) - Built-in functions +10. [Advanced Topics](guide/10-advanced.md) - Traits, generics, optimization + +### [Language Reference](reference/syntax.md) +Complete syntax and semantics reference. + +- [Syntax](reference/syntax.md) - Grammar and syntax rules +- [Types](reference/types.md) - Type system details +- [Effects](reference/effects.md) - Effect system reference +- [Standard Library](reference/stdlib.md) - All built-in functions + +### [Tutorials](tutorials/README.md) +Project-based learning. + +**Standard Programs:** +- [Calculator](tutorials/calculator.md) - Basic REPL calculator +- [Todo App](tutorials/todo.md) - File I/O and data structures +- [HTTP Client](tutorials/http-client.md) - Fetching web data + +**Effect Showcases:** +- [Dependency Injection](tutorials/dependency-injection.md) - Testing with effects +- [State Machines](tutorials/state-machines.md) - Modeling state with effects +- [Parser Combinators](tutorials/parsers.md) - Effects for backtracking + +--- + +## Project Ideas + +Once you've learned the basics, try building: + +### Beginner +- **Unit converter** - Temperatures, distances, weights +- **Word counter** - Count words, lines, characters in files +- **Quiz game** - Random questions with scoring + +### Intermediate +- **Markdown parser** - Parse and render Markdown +- **Task manager** - CLI todo list with file persistence +- **API client** - Fetch and display data from a REST API + +### Advanced +- **Effect-based testing framework** - Use effects for test isolation +- **Configuration DSL** - Effect-based config with validation +- **Interpreter** - Build a small language interpreter + +### Showcasing Effects +- **Transactional file system** - Rollback on failure using effects +- **Mock HTTP for testing** - Swap real HTTP with mock handlers +- **Async simulation** - Model concurrency with effect handlers +- **Capability-based security** - Effects as capabilities + +--- + +## Philosophy + +Lux is designed around these principles: + +1. **Explicitness over magic** - Effects in the type signature, not hidden +2. **Composition over inheritance** - Effects combine freely +3. **Safety with escape hatches** - Type safety by default, but practical +4. **Functional core, imperative shell** - Pure logic, effectful boundaries + +--- + +## Getting Help + +- **REPL**: Type `:help` for commands +- **LSP**: Full IDE support in VS Code, Neovim +- **Examples**: See `examples/` directory +- **Issues**: Report bugs on GitHub + +--- + +*Lux: Where effects are first-class citizens.* diff --git a/docs/guide/01-introduction.md b/docs/guide/01-introduction.md new file mode 100644 index 0000000..43cf30d --- /dev/null +++ b/docs/guide/01-introduction.md @@ -0,0 +1,222 @@ +# Chapter 1: Introduction to Lux + +Welcome to Lux, a functional programming language where side effects are first-class citizens. + +## Why Lux? + +Every program does more than compute. It reads files, makes network requests, prints output, handles errors. In most languages, these *effects* are invisible—a function's type doesn't tell you what it might do. + +Lux changes this. Effects are: +- **Visible** in the type signature +- **Controllable** via handlers +- **Composable** without boilerplate + +This isn't just academic. It means: +- Tests can swap real I/O for mocks—automatically +- Error handling is explicit, not exceptional +- Dependencies are injected through the type system + +## Installation + +### From Source (Cargo) + +```bash +git clone https://github.com/your-org/lux +cd lux +cargo build --release + +# Add to PATH +export PATH="$PATH:$(pwd)/target/release" +``` + +### With Nix + +```bash +# Enter development shell +nix develop + +# Or build +nix build +./result/bin/lux +``` + +### Verify Installation + +```bash +$ lux --version +Lux 0.1.0 + +$ lux --help +Usage: lux [OPTIONS] [FILE] + +Options: + -c, --check Type check without running + --lsp Start LSP server + --repl Start interactive REPL + -h, --help Print help +``` + +## Your First Program + +Create a file called `hello.lux`: + +```lux +// hello.lux - Your first Lux program + +fn main(): Unit with {Console} = + Console.print("Hello, Lux!") + +let output = run main() with {} +``` + +Run it: + +```bash +$ lux hello.lux +Hello, Lux! +``` + +Let's break this down: + +### The Function Signature + +```lux +fn main(): Unit with {Console} +``` + +- `fn main()` - A function named `main` with no parameters +- `: Unit` - Returns `Unit` (like `void`) +- `with {Console}` - **Uses the Console effect** + +That last part is key. The type tells us this function prints to the console. A function without `with {...}` is *pure*—it can only compute. + +### The Body + +```lux +Console.print("Hello, Lux!") +``` + +`Console.print` is an *effect operation*. It's not a regular function—it's a request to the environment to print something. + +### Running Effects + +```lux +let output = run main() with {} +``` + +The `run ... with {}` expression executes a computation with its effects. The `{}` means "use the default handlers for all effects." Console's default handler prints to stdout. + +## The REPL + +Lux has an interactive mode: + +```bash +$ lux +Lux v0.1.0 - Type :help for commands + +> +``` + +Try some expressions: + +``` +> 1 + 2 +3 + +> "Hello, " + "World" +"Hello, World" + +> [1, 2, 3] +[1, 2, 3] + +> List.map([1, 2, 3], fn(x: Int): Int => x * 2) +[2, 4, 6] +``` + +Define functions: + +``` +> fn square(x: Int): Int = x * x + + +> square(5) +25 + +> fn greet(name: String): Unit with {Console} = Console.print("Hi, " + name) + + +> run greet("Alice") with {} +Hi, Alice +``` + +REPL commands: + +``` +:help - Show help +:type e - Show type of expression +:quit - Exit REPL +:clear - Clear screen +``` + +## A Slightly Bigger Program + +Let's write a program that asks for your name: + +```lux +// greet.lux + +fn askName(): String with {Console} = { + Console.print("What's your name?") + Console.readLine() +} + +fn main(): Unit with {Console} = { + let name = askName() + Console.print("Hello, " + name + "!") +} + +let output = run main() with {} +``` + +```bash +$ lux greet.lux +What's your name? +> Alice +Hello, Alice! +``` + +Notice how: +- `askName` returns `String` but has `with {Console}` because it does I/O +- Both functions declare their effects +- The `run` at the end provides the runtime + +## Pure vs Effectful + +Here's the key insight. Compare: + +```lux +// Pure - no effects, only computes +fn add(a: Int, b: Int): Int = a + b + +// Effectful - uses Console +fn printSum(a: Int, b: Int): Unit with {Console} = + Console.print(toString(a + b)) +``` + +You can call `add` from anywhere. But `printSum` can only be called from: +1. Another function that declares `Console` +2. Inside a `run ... with {}` block + +This is the effect discipline. Effects propagate up until handled. + +## What's Next + +Now that you can run programs, let's learn: + +- [Chapter 2: Basic Types](02-basic-types.md) - Numbers, strings, booleans +- [Chapter 3: Functions](03-functions.md) - Definitions, closures, composition +- [Chapter 4: Data Types](04-data-types.md) - ADTs and pattern matching + +Or jump ahead to what makes Lux special: + +- [Chapter 5: Effects](05-effects.md) - The core innovation diff --git a/docs/guide/02-basic-types.md b/docs/guide/02-basic-types.md new file mode 100644 index 0000000..7206a91 --- /dev/null +++ b/docs/guide/02-basic-types.md @@ -0,0 +1,248 @@ +# Chapter 2: Basic Types + +Lux is statically typed with full type inference. You rarely need to write types—the compiler figures them out—but understanding them helps. + +## Primitive Types + +### Int + +64-bit signed integers: + +```lux +let x = 42 +let y = -17 +let big = 9_223_372_036_854_775_807 // Underscores for readability + +// Arithmetic +x + y // 25 +x - y // 59 +x * y // -714 +x / y // -2 (integer division) +x % y // 8 (remainder) +``` + +### Float + +64-bit floating point (IEEE 754): + +```lux +let pi = 3.14159 +let e = 2.718 + +pi * 2.0 // 6.28318 +e / 2.0 // 1.359 +``` + +### Bool + +Boolean values: + +```lux +let yes = true +let no = false + +// Operators +true && false // false (and) +true || false // true (or) +!true // false (not) + +// Comparison +5 > 3 // true +5 == 5 // true +5 != 3 // true +5 <= 5 // true +``` + +### String + +UTF-8 strings: + +```lux +let greeting = "Hello" +let name = "World" + +// Concatenation +greeting + ", " + name + "!" // "Hello, World!" + +// String interpolation +let message = "The answer is ${40 + 2}" // "The answer is 42" + +// Multiline +let poem = " + Roses are red, + Violets are blue, + Lux has effects, + And so can you. +" +``` + +### Char + +Single Unicode characters: + +```lux +let letter = 'A' +let emoji = '🎉' +``` + +### Unit + +The type with only one value, `()`. Used when a function doesn't return anything meaningful: + +```lux +fn printHello(): Unit with {Console} = + Console.print("Hello") + +let nothing: Unit = () +``` + +## Type Annotations + +Usually optional, but sometimes helpful: + +```lux +// Inferred +let x = 42 // Int +let s = "hello" // String + +// Explicit +let x: Int = 42 +let s: String = "hello" + +// Required when ambiguous +let empty: List = [] +``` + +## Lists + +Ordered collections of the same type: + +```lux +let numbers = [1, 2, 3, 4, 5] +let words = ["hello", "world"] +let empty: List = [] + +// Operations (from List module) +List.length(numbers) // 5 +List.head(numbers) // Some(1) +List.tail(numbers) // [2, 3, 4, 5] +List.map(numbers, fn(x: Int): Int => x * 2) // [2, 4, 6, 8, 10] +List.filter(numbers, fn(x: Int): Bool => x > 2) // [3, 4, 5] +List.fold(numbers, 0, fn(acc: Int, x: Int): Int => acc + x) // 15 +``` + +## Option + +For values that might not exist: + +```lux +let some_value: Option = Some(42) +let no_value: Option = None + +// Pattern matching +fn describe(opt: Option): String = + match opt { + Some(n) => "Got: " + toString(n), + None => "Nothing" + } + +// Option operations +Option.map(Some(5), fn(x: Int): Int => x * 2) // Some(10) +Option.map(None, fn(x: Int): Int => x * 2) // None +Option.getOrElse(Some(5), 0) // 5 +Option.getOrElse(None, 0) // 0 +``` + +## Result + +For operations that can fail: + +```lux +let success: Result = Ok(42) +let failure: Result = Err("Something went wrong") + +// Pattern matching +fn handle(r: Result): String = + match r { + Ok(n) => "Success: " + toString(n), + Err(e) => "Error: " + e + } + +// Result operations +Result.map(Ok(5), fn(x: Int): Int => x * 2) // Ok(10) +Result.map(Err("oops"), fn(x: Int): Int => x * 2) // Err("oops") +``` + +## Tuples + +Fixed-size collections of different types: + +```lux +let pair = (1, "hello") +let triple = (true, 42, "world") + +// Access by pattern matching +let (x, y) = pair +// x = 1, y = "hello" +``` + +## Records + +Named fields: + +```lux +let person = { name: "Alice", age: 30 } + +// Access +person.name // "Alice" +person.age // 30 + +// With type annotation +type Person = { name: String, age: Int } +let bob: Person = { name: "Bob", age: 25 } +``` + +## Type Conversion + +```lux +// To String +toString(42) // "42" +toString(true) // "true" +toString([1,2,3]) // "[1, 2, 3]" + +// String to Int (via Console effect) +let n = Console.readInt() +``` + +## Type Checking + +The compiler catches type errors: + +```lux +let x: Int = "hello" // Error: expected Int, got String + +fn add(a: Int, b: Int): Int = a + b +add(1, "two") // Error: expected Int, got String + +let nums = [1, 2, "three"] // Error: list elements must have same type +``` + +## Summary + +| Type | Example | Description | +|------|---------|-------------| +| `Int` | `42` | 64-bit integer | +| `Float` | `3.14` | 64-bit float | +| `Bool` | `true` | Boolean | +| `String` | `"hello"` | UTF-8 string | +| `Char` | `'A'` | Unicode character | +| `Unit` | `()` | No meaningful value | +| `List` | `[1, 2, 3]` | Ordered collection | +| `Option` | `Some(42)` | Optional value | +| `Result` | `Ok(42)` | Success or failure | +| `(A, B)` | `(1, "hi")` | Tuple | +| `{...}` | `{x: 1}` | Record | + +## Next + +[Chapter 3: Functions](03-functions.md) - Learn to define and compose functions. diff --git a/docs/guide/03-functions.md b/docs/guide/03-functions.md new file mode 100644 index 0000000..ea8049c --- /dev/null +++ b/docs/guide/03-functions.md @@ -0,0 +1,272 @@ +# Chapter 3: Functions + +Functions are the building blocks of Lux programs. They're first-class values—you can pass them around, return them, and store them in data structures. + +## Defining Functions + +Basic syntax: + +```lux +fn name(param1: Type1, param2: Type2): ReturnType = body +``` + +Examples: + +```lux +fn add(a: Int, b: Int): Int = a + b + +fn greet(name: String): String = "Hello, " + name + +fn isEven(n: Int): Bool = n % 2 == 0 +``` + +## Single Expression vs Block Body + +Simple functions use `=`: + +```lux +fn square(x: Int): Int = x * x +``` + +Complex functions use `= { ... }`: + +```lux +fn classify(n: Int): String = { + let abs_n = if n < 0 then -n else n + if abs_n == 0 then "zero" + else if abs_n < 10 then "small" + else "large" +} +``` + +The last expression in a block is the return value. No `return` keyword needed. + +## Type Inference + +Return types can often be inferred: + +```lux +fn add(a: Int, b: Int) = a + b // Returns Int +fn not(b: Bool) = !b // Returns Bool +``` + +But parameter types are always required: + +```lux +fn double(x) = x * 2 // Error: parameter type required +``` + +## Anonymous Functions (Lambdas) + +Functions without names: + +```lux +fn(x: Int): Int => x * 2 +``` + +Used with higher-order functions: + +```lux +List.map([1, 2, 3], fn(x: Int): Int => x * 2) // [2, 4, 6] + +List.filter([1, 2, 3, 4], fn(x: Int): Bool => x > 2) // [3, 4] +``` + +Store in variables: + +```lux +let double = fn(x: Int): Int => x * 2 +double(5) // 10 +``` + +## Higher-Order Functions + +Functions that take or return functions: + +```lux +// Takes a function +fn apply(f: fn(Int): Int, x: Int): Int = f(x) + +apply(fn(x: Int): Int => x + 1, 5) // 6 + +// Returns a function +fn makeAdder(n: Int): fn(Int): Int = + fn(x: Int): Int => x + n + +let add10 = makeAdder(10) +add10(5) // 15 +add10(20) // 30 +``` + +## Closures + +Functions capture their environment: + +```lux +fn counter(): fn(): Int with {State} = { + let count = 0 + fn(): Int with {State} => { + State.put(State.get() + 1) + State.get() + } +} + +// The returned function remembers `count` +``` + +More practical example: + +```lux +fn makeMultiplier(factor: Int): fn(Int): Int = + fn(x: Int): Int => x * factor + +let triple = makeMultiplier(3) +triple(4) // 12 +triple(7) // 21 +``` + +## Recursion + +Functions can call themselves: + +```lux +fn factorial(n: Int): Int = + if n <= 1 then 1 + else n * factorial(n - 1) + +factorial(5) // 120 +``` + +## Tail Call Optimization + +Lux optimizes tail-recursive functions: + +```lux +// Not tail-recursive (stack grows) +fn factorial(n: Int): Int = + if n <= 1 then 1 + else n * factorial(n - 1) // Must multiply AFTER recursive call + +// Tail-recursive (constant stack) +fn factorialTail(n: Int, acc: Int): Int = + if n <= 1 then acc + else factorialTail(n - 1, n * acc) // Recursive call is LAST operation + +fn factorial(n: Int): Int = factorialTail(n, 1) +``` + +The tail-recursive version won't overflow the stack. + +## Function Composition + +Combine functions: + +```lux +fn compose(f: fn(B): C, g: fn(A): B): fn(A): C = + fn(x: A): C => f(g(x)) + +fn addOne(x: Int): Int = x + 1 +fn double(x: Int): Int = x * 2 + +let addOneThenDouble = compose(double, addOne) +addOneThenDouble(5) // 12 = (5 + 1) * 2 +``` + +## Partial Application + +Create new functions by fixing some arguments: + +```lux +fn add(a: Int, b: Int): Int = a + b + +// Manually partial apply +fn add5(b: Int): Int = add(5, b) + +add5(3) // 8 +``` + +## Pipeline Style + +Chain operations readably: + +```lux +// Without pipeline +toString(List.sum(List.map(List.filter([1,2,3,4,5], isEven), square))) + +// With intermediate variables +let nums = [1, 2, 3, 4, 5] +let evens = List.filter(nums, isEven) +let squared = List.map(evens, square) +let total = List.sum(squared) +toString(total) +``` + +## Functions with Effects + +Functions that perform effects declare them: + +```lux +fn pureAdd(a: Int, b: Int): Int = a + b // No effects + +fn printAdd(a: Int, b: Int): Unit with {Console} = { + let sum = a + b + Console.print("Sum: " + toString(sum)) +} +``` + +Effects propagate: + +```lux +fn helper(): Int with {Console} = { + Console.print("Computing...") + 42 +} + +// Must also declare Console since it calls helper +fn main(): Int with {Console} = { + let x = helper() + x * 2 +} +``` + +## Generic Functions + +Functions that work with any type: + +```lux +fn identity(x: T): T = x + +identity(42) // 42 +identity("hello") // "hello" +identity(true) // true + +fn pair(a: A, b: B): (A, B) = (a, b) + +pair(1, "one") // (1, "one") +``` + +## Summary + +```lux +// Basic function +fn name(param: Type): Return = body + +// Lambda +fn(x: Int): Int => x * 2 + +// Higher-order (takes function) +fn apply(f: fn(Int): Int, x: Int): Int = f(x) + +// Higher-order (returns function) +fn makeAdder(n: Int): fn(Int): Int = fn(x: Int): Int => x + n + +// Generic +fn identity(x: T): T = x + +// With effects +fn greet(name: String): Unit with {Console} = Console.print("Hi " + name) +``` + +## Next + +[Chapter 4: Data Types](04-data-types.md) - Algebraic data types and pattern matching. diff --git a/docs/guide/04-data-types.md b/docs/guide/04-data-types.md new file mode 100644 index 0000000..c16587c --- /dev/null +++ b/docs/guide/04-data-types.md @@ -0,0 +1,331 @@ +# Chapter 4: Data Types + +Lux has algebraic data types (ADTs)—a powerful way to model data with variants and pattern matching. + +## Defining Types + +### Enums (Sum Types) + +A type that can be one of several variants: + +```lux +type Color = + | Red + | Green + | Blue + +let c: Color = Red +``` + +### Variants with Data + +Variants can carry data: + +```lux +type Shape = + | Circle(Int) // radius + | Rectangle(Int, Int) // width, height + | Point + +let s1 = Circle(5) +let s2 = Rectangle(10, 20) +let s3 = Point +``` + +### Named Fields + +For clarity, use record variants: + +```lux +type Person = + | Person { name: String, age: Int } + +let alice = Person { name: "Alice", age: 30 } +``` + +## Pattern Matching + +The `match` expression destructures data: + +```lux +fn colorName(c: Color): String = + match c { + Red => "red", + Green => "green", + Blue => "blue" + } +``` + +### Extracting Data + +```lux +fn area(s: Shape): Int = + match s { + Circle(r) => 3 * r * r, // Approximate π as 3 + Rectangle(w, h) => w * h, + Point => 0 + } + +area(Circle(5)) // 75 +area(Rectangle(4, 5)) // 20 +``` + +### Exhaustiveness + +The compiler ensures you handle all cases: + +```lux +fn colorName(c: Color): String = + match c { + Red => "red", + Green => "green" + // Error: non-exhaustive pattern, missing Blue + } +``` + +### Wildcard Pattern + +Use `_` to match anything: + +```lux +fn isRed(c: Color): Bool = + match c { + Red => true, + _ => false // Matches Green, Blue, anything else + } +``` + +### Guards + +Add conditions to patterns: + +```lux +fn classify(n: Int): String = + match n { + 0 => "zero", + n if n > 0 => "positive", + _ => "negative" + } +``` + +### Nested Patterns + +Match deep structures: + +```lux +type Expr = + | Num(Int) + | Add(Expr, Expr) + | Mul(Expr, Expr) + +fn simplify(e: Expr): Expr = + match e { + Add(Num(0), x) => x, // 0 + x = x + Add(x, Num(0)) => x, // x + 0 = x + Mul(Num(0), _) => Num(0), // 0 * x = 0 + Mul(_, Num(0)) => Num(0), // x * 0 = 0 + Mul(Num(1), x) => x, // 1 * x = x + Mul(x, Num(1)) => x, // x * 1 = x + _ => e // No simplification + } +``` + +## Built-in ADTs + +### Option + +For optional values: + +```lux +type Option = + | Some(T) + | None +``` + +Usage: + +```lux +fn safeDivide(a: Int, b: Int): Option = + if b == 0 then None + else Some(a / b) + +fn showResult(opt: Option): String = + match opt { + Some(n) => "Result: " + toString(n), + None => "Cannot divide by zero" + } + +showResult(safeDivide(10, 2)) // "Result: 5" +showResult(safeDivide(10, 0)) // "Cannot divide by zero" +``` + +### Result + +For operations that can fail with an error: + +```lux +type Result = + | Ok(T) + | Err(E) +``` + +Usage: + +```lux +fn parseAge(s: String): Result = + // Simplified - assume we have a real parser + if s == "42" then Ok(42) + else Err("Invalid age: " + s) + +fn handleAge(r: Result): String = + match r { + Ok(age) => "Age is " + toString(age), + Err(msg) => "Error: " + msg + } +``` + +### List + +Lists are built-in but conceptually: + +```lux +type List = + | Nil + | Cons(T, List) +``` + +Pattern match on lists: + +```lux +fn sum(nums: List): Int = + match nums { + [] => 0, + [x, ...rest] => x + sum(rest) + } + +fn length(list: List): Int = + match list { + [] => 0, + [_, ...rest] => 1 + length(rest) + } +``` + +## Recursive Types + +Types can reference themselves: + +```lux +type Tree = + | Leaf(T) + | Node(Tree, Tree) + +fn sumTree(t: Tree): Int = + match t { + Leaf(n) => n, + Node(left, right) => sumTree(left) + sumTree(right) + } + +let tree = Node(Node(Leaf(1), Leaf(2)), Leaf(3)) +sumTree(tree) // 6 +``` + +## Type Aliases + +Give names to existing types: + +```lux +type UserId = Int +type Username = String +type UserMap = List<(UserId, Username)> +``` + +## Records + +Anonymous record types: + +```lux +let point = { x: 10, y: 20 } +point.x // 10 +point.y // 20 + +// With type annotation +type Point = { x: Int, y: Int } +let p: Point = { x: 5, y: 10 } +``` + +### Record Update + +Create new records based on existing ones: + +```lux +let p1 = { x: 10, y: 20 } +let p2 = { ...p1, x: 15 } // { x: 15, y: 20 } +``` + +## Practical Example: Expression Evaluator + +```lux +type Expr = + | Num(Int) + | Add(Expr, Expr) + | Sub(Expr, Expr) + | Mul(Expr, Expr) + | Div(Expr, Expr) + +fn eval(e: Expr): Result = + match e { + Num(n) => Ok(n), + Add(a, b) => { + match (eval(a), eval(b)) { + (Ok(x), Ok(y)) => Ok(x + y), + (Err(e), _) => Err(e), + (_, Err(e)) => Err(e) + } + }, + Sub(a, b) => { + match (eval(a), eval(b)) { + (Ok(x), Ok(y)) => Ok(x - y), + (Err(e), _) => Err(e), + (_, Err(e)) => Err(e) + } + }, + Mul(a, b) => { + match (eval(a), eval(b)) { + (Ok(x), Ok(y)) => Ok(x * y), + (Err(e), _) => Err(e), + (_, Err(e)) => Err(e) + } + }, + Div(a, b) => { + match (eval(a), eval(b)) { + (Ok(_), Ok(0)) => Err("Division by zero"), + (Ok(x), Ok(y)) => Ok(x / y), + (Err(e), _) => Err(e), + (_, Err(e)) => Err(e) + } + } + } + +// (10 + 5) * 2 +let expr = Mul(Add(Num(10), Num(5)), Num(2)) +eval(expr) // Ok(30) + +// 10 / 0 +let bad = Div(Num(10), Num(0)) +eval(bad) // Err("Division by zero") +``` + +## Summary + +| Concept | Syntax | Example | +|---------|--------|---------| +| Enum | `type T = \| A \| B` | `type Bool = \| True \| False` | +| With data | `\| Variant(Type)` | `\| Some(Int)` | +| Match | `match x { ... }` | `match opt { Some(n) => n, None => 0 }` | +| Wildcard | `_` | `_ => "default"` | +| Guard | `pattern if cond` | `n if n > 0 => "positive"` | +| Record | `{ field: value }` | `{ x: 10, y: 20 }` | + +## Next + +[Chapter 5: Effects](05-effects.md) - The core innovation of Lux. diff --git a/docs/guide/05-effects.md b/docs/guide/05-effects.md new file mode 100644 index 0000000..769ad00 --- /dev/null +++ b/docs/guide/05-effects.md @@ -0,0 +1,350 @@ +# Chapter 5: Effects + +This is where Lux gets interesting. Effects are the core innovation—they make side effects explicit, controllable, and composable. + +## The Problem with Side Effects + +In most languages, functions can do *anything*: + +```javascript +// JavaScript - what does this do? +function process(x) { + return x * 2; +} +``` + +It looks pure, but it could: +- Print to console +- Write to a file +- Make HTTP requests +- Throw exceptions +- Modify global state +- Launch missiles + +You can't tell from the signature. You have to read the implementation. + +## The Lux Solution + +In Lux, effects are declared: + +```lux +// Pure - only computes +fn double(x: Int): Int = x * 2 + +// Uses Console - you can see it +fn printDouble(x: Int): Unit with {Console} = + Console.print(toString(x * 2)) +``` + +The `with {Console}` tells you this function interacts with the console. It's part of the type. + +## Built-in Effects + +Lux provides several built-in effects: + +| Effect | Operations | Purpose | +|--------|------------|---------| +| `Console` | `print`, `readLine`, `readInt` | Terminal I/O | +| `Fail` | `fail` | Early termination | +| `State` | `get`, `put` | Mutable state | +| `Random` | `int`, `float`, `bool` | Random numbers | +| `File` | `read`, `write`, `exists` | File system | +| `Process` | `exec`, `env`, `args` | System processes | +| `Http` | `get`, `post`, `put`, `delete` | HTTP client | + +Example usage: + +```lux +fn main(): Unit with {Console, Random} = { + let n = Random.int(1, 100) + Console.print("Random number: " + toString(n)) +} + +let output = run main() with {} +``` + +## Effect Propagation + +Effects propagate up the call stack: + +```lux +fn helper(): Int with {Console} = { + Console.print("In helper") + 42 +} + +// Must declare Console because it calls helper +fn caller(): Int with {Console} = { + let x = helper() + x * 2 +} + +// Error: caller uses Console but doesn't declare it +fn broken(): Int = { + caller() // Error! +} +``` + +The rule: if you call a function with effect E, you must either: +1. Declare E in your signature +2. Handle E with a `run ... with {}` block + +## Running Effects + +The `run ... with {}` block executes effectful code: + +```lux +fn greet(): Unit with {Console} = + Console.print("Hello!") + +// Execute with default handlers +let result = run greet() with {} +``` + +For built-in effects, `with {}` uses the default implementations (real console, real files, etc.). + +## Custom Effects + +You can define your own effects: + +```lux +effect Logger { + fn log(level: String, message: String): Unit + fn getLevel(): String +} +``` + +This declares an effect with two operations. To use it: + +```lux +fn processData(data: Int): Int with {Logger} = { + Logger.log("info", "Starting processing") + let result = data * 2 + Logger.log("debug", "Result: " + toString(result)) + result +} +``` + +But this won't run yet—we need a *handler*. + +## Handlers + +Handlers define how effect operations behave: + +```lux +handler consoleLogger: Logger { + fn log(level, message) = { + Console.print("[" + level + "] " + message) + resume(()) + } + fn getLevel() = resume("debug") +} +``` + +Key concept: **`resume(value)`** continues the computation with `value` as the result of the effect operation. + +Now we can run: + +```lux +fn main(): Unit with {Console} = { + let result = run processData(21) with { + Logger = consoleLogger + } + Console.print("Final: " + toString(result)) +} + +let output = run main() with {} +``` + +Output: +``` +[info] Starting processing +[debug] Result: 42 +Final: 42 +``` + +## The Power of Handlers + +### Different Implementations + +Same code, different behaviors: + +```lux +// Console logging +handler consoleLogger: Logger { + fn log(level, msg) = { + Console.print("[" + level + "] " + msg) + resume(()) + } + fn getLevel() = resume("debug") +} + +// Silent (for testing) +handler silentLogger: Logger { + fn log(level, msg) = resume(()) + fn getLevel() = resume("none") +} + +// Collecting logs +handler collectingLogger: Logger { + fn log(level, msg) = { + State.put(State.get() + "[" + level + "] " + msg + "\n") + resume(()) + } + fn getLevel() = resume("all") +} +``` + +### Resumable Operations + +Unlike exceptions, handlers can *continue* the computation: + +```lux +effect Ask { + fn ask(prompt: String): String +} + +fn survey(): String with {Ask} = { + let name = Ask.ask("Name?") + let age = Ask.ask("Age?") + name + " is " + age + " years old" +} + +// Handler that provides answers +handler mockAnswers: Ask { + fn ask(prompt) = + if String.contains(prompt, "Name") then resume("Alice") + else resume("30") +} + +run survey() with { Ask = mockAnswers } +// Returns: "Alice is 30 years old" +``` + +The computation pauses at `Ask.ask`, the handler provides a value, and `resume` continues from where it left off. + +## Effect Composition + +Multiple effects combine naturally: + +```lux +fn program(): Int with {Console, Random, Logger} = { + Logger.log("info", "Starting") + let n = Random.int(1, 10) + Console.print("Got: " + toString(n)) + Logger.log("debug", "Returning " + toString(n)) + n +} + +let result = run program() with { + Logger = consoleLogger +} +``` + +No monad transformers, no lifting, no complexity. Effects just work together. + +## Why This Matters + +### 1. Testability + +```lux +// Production code +fn fetchUser(id: Int): String with {Http} = + Http.get("https://api.example.com/users/" + toString(id)) + +// Test with mock HTTP +handler mockHttp: Http { + fn get(url) = resume("{\"name\": \"Test User\"}") + // ... other operations +} + +// Test runs without network +let result = run fetchUser(1) with { Http = mockHttp } +``` + +### 2. Explicit Dependencies + +```lux +// You can see exactly what this function needs +fn processOrder(order: Order): Receipt with {Database, Logger, Email} +``` + +### 3. Controlled Side Effects + +```lux +// This function is pure - it CAN'T do I/O +fn calculateTotal(items: List): Int = + List.fold(items, 0, fn(acc: Int, item: Item): Int => acc + item.price) + +// Compile error if you try to add Console.print here +``` + +## Common Patterns + +### Dependency Injection + +```lux +effect Database { + fn query(sql: String): List + fn execute(sql: String): Int +} + +fn getUsers(): List with {Database} = + Database.query("SELECT * FROM users") + +// Production +handler postgresDb: Database { /* real implementation */ } + +// Testing +handler mockDb: Database { + fn query(sql) = resume([mockRow1, mockRow2]) + fn execute(sql) = resume(1) +} +``` + +### Configuration + +```lux +effect Config { + fn get(key: String): String +} + +fn appUrl(): String with {Config} = + Config.get("APP_URL") + +handler envConfig: Config { + fn get(key) = resume(Process.env(key)) +} + +handler testConfig: Config { + fn get(key) = resume("http://localhost:8080") +} +``` + +### Early Return + +```lux +fn validateUser(user: User): User with {Fail} = { + if user.name == "" then Fail.fail("Name required") + else if user.age < 0 then Fail.fail("Invalid age") + else user +} + +// Fail stops execution +let result = run validateUser(invalidUser) with {} +``` + +## Summary + +| Concept | Syntax | +|---------|--------| +| Declare effect | `effect Name { fn op(): Type }` | +| Use effect | `fn f(): T with {Effect}` | +| Effect operation | `Effect.operation(args)` | +| Define handler | `handler name: Effect { fn op(...) = ... }` | +| Resume | `resume(value)` | +| Run with handler | `run expr with { Effect = handler }` | + +## Next + +[Chapter 6: Handlers](06-handlers.md) - Deep dive into handler patterns. diff --git a/docs/guide/06-handlers.md b/docs/guide/06-handlers.md new file mode 100644 index 0000000..f72d674 --- /dev/null +++ b/docs/guide/06-handlers.md @@ -0,0 +1,348 @@ +# Chapter 6: Handlers + +Handlers are where effects become powerful. They define *how* effect operations behave, and they can do surprising things. + +## Basic Handler Structure + +```lux +handler handlerName: EffectName { + fn operation1(param1, param2) = { + // Implementation + resume(result) + } + fn operation2(param) = { + // Implementation + resume(result) + } +} +``` + +Key points: +- Handlers implement all operations of an effect +- `resume(value)` continues the computation with `value` +- Handlers can use other effects in their implementations + +## Understanding Resume + +`resume` is the key to handler power. It continues the computation from where the effect was called. + +```lux +effect Ask { + fn ask(prompt: String): Int +} + +fn computation(): Int with {Ask} = { + let x = Ask.ask("first") // Pauses here + let y = Ask.ask("second") // Then here + x + y +} + +handler doubler: Ask { + fn ask(prompt) = resume(2) // Always returns 2 +} + +run computation() with { Ask = doubler } +// Returns: 4 (2 + 2) +``` + +The flow: +1. `computation` calls `Ask.ask("first")` +2. Handler receives control, calls `resume(2)` +3. `computation` continues with `x = 2` +4. `computation` calls `Ask.ask("second")` +5. Handler receives control, calls `resume(2)` +6. `computation` continues with `y = 2` +7. Returns `4` + +## Handlers That Don't Resume + +Not all handlers must resume. Some can abort: + +```lux +effect Validate { + fn check(condition: Bool, message: String): Unit +} + +fn validateAge(age: Int): String with {Validate} = { + Validate.check(age >= 0, "Age cannot be negative") + Validate.check(age < 150, "Age seems unrealistic") + "Valid age: " + toString(age) +} + +handler strictValidator: Validate { + fn check(cond, msg) = + if cond then resume(()) + else "Validation failed: " + msg // Returns early, doesn't resume +} + +run validateAge(-5) with { Validate = strictValidator } +// Returns: "Validation failed: Age cannot be negative" +``` + +## Handlers Using Other Effects + +Handlers can use effects in their implementation: + +```lux +effect Logger { + fn log(msg: String): Unit +} + +handler consoleLogger: Logger { + fn log(msg) = { + Console.print("[LOG] " + msg) // Uses Console effect + resume(()) + } +} + +fn main(): Unit with {Console} = { + let result = run { + Logger.log("Hello") + Logger.log("World") + 42 + } with { Logger = consoleLogger } + Console.print("Result: " + toString(result)) +} +``` + +## Stateful Handlers + +Handlers can maintain state using the State effect: + +```lux +effect Counter { + fn increment(): Unit + fn get(): Int +} + +handler counterHandler: Counter { + fn increment() = { + State.put(State.get() + 1) + resume(()) + } + fn get() = resume(State.get()) +} + +fn counting(): Int with {Counter} = { + Counter.increment() + Counter.increment() + Counter.increment() + Counter.get() +} + +fn main(): Unit with {Console} = { + let result = run { + run counting() with { Counter = counterHandler } + } with { State = 0 } + Console.print("Count: " + toString(result)) // Count: 3 +} +``` + +## Handler Patterns + +### The Reader Pattern + +Provide read-only context: + +```lux +effect Config { + fn get(key: String): String +} + +handler envConfig: Config { + fn get(key) = resume(Process.env(key)) +} + +handler mapConfig(settings: Map): Config { + fn get(key) = resume(Map.getOrDefault(settings, key, "")) +} +``` + +### The Writer Pattern + +Accumulate output: + +```lux +effect Log { + fn write(msg: String): Unit +} + +handler collectLogs: Log { + fn write(msg) = { + State.put(State.get() + msg + "\n") + resume(()) + } +} + +fn program(): Int with {Log} = { + Log.write("Starting") + let result = 42 + Log.write("Done") + result +} + +// Get both result and logs +let (result, logs) = run { + run program() with { Log = collectLogs } + let logs = State.get() + (result, logs) +} with { State = "" } +``` + +### The Exception Pattern + +Early termination with cleanup: + +```lux +effect Fail { + fn fail(msg: String): Unit +} + +handler catchFail: Fail { + fn fail(msg) = Err(msg) // Don't resume, return error +} + +fn riskyOperation(): Int with {Fail} = { + if Random.bool() then Fail.fail("Bad luck!") + else 42 +} + +let result: Result = run riskyOperation() with { Fail = catchFail } +``` + +### The Choice Pattern + +Non-determinism: + +```lux +effect Choice { + fn choose(options: List): T +} + +fn picker(): Int with {Choice} = { + let x = Choice.choose([1, 2, 3]) + let y = Choice.choose([10, 20]) + x + y +} + +// Handler that returns first option +handler firstChoice: Choice { + fn choose(opts) = resume(List.head(opts)) +} + +// Handler that returns all combinations +handler allChoices: Choice { + fn choose(opts) = + List.flatMap(opts, fn(opt: T): List => resume(opt)) +} +``` + +## Combining Multiple Handlers + +Multiple effects, multiple handlers: + +```lux +effect Logger { fn log(msg: String): Unit } +effect Counter { fn count(): Int } + +fn program(): Int with {Logger, Counter} = { + Logger.log("Starting") + let n = Counter.count() + Logger.log("Got " + toString(n)) + n * 2 +} + +handler myLogger: Logger { + fn log(msg) = { Console.print(msg); resume(()) } +} + +handler myCounter: Counter { + fn count() = resume(42) +} + +let result = run program() with { + Logger = myLogger, + Counter = myCounter +} +``` + +## Handler Scope + +Handlers apply to their `run` block: + +```lux +handler loudLogger: Logger { + fn log(msg) = { Console.print("!!! " + msg + " !!!"); resume(()) } +} + +handler quietLogger: Logger { + fn log(msg) = resume(()) // Silent +} + +fn program(): Unit with {Logger, Console} = { + Logger.log("Outer") + + let inner = run { + Logger.log("Inner") + 42 + } with { Logger = quietLogger } + + Logger.log("Back to outer") + Console.print("Inner result: " + toString(inner)) +} + +run program() with { Logger = loudLogger } +// Output: +// !!! Outer !!! +// !!! Back to outer !!! +// Inner result: 42 +``` + +The inner `run` uses `quietLogger`, so "Inner" is silent. + +## Real-World Example: Database Testing + +```lux +effect Database { + fn query(sql: String): List + fn execute(sql: String): Int +} + +// Production handler using real database +handler postgresDb(conn: Connection): Database { + fn query(sql) = resume(Postgres.query(conn, sql)) + fn execute(sql) = resume(Postgres.execute(conn, sql)) +} + +// Test handler using in-memory data +handler mockDb(data: List): Database { + fn query(sql) = resume(data) + fn execute(sql) = resume(1) +} + +fn getUserCount(): Int with {Database} = { + let rows = Database.query("SELECT COUNT(*) FROM users") + extractCount(rows) +} + +// Production +run getUserCount() with { Database = postgresDb(realConnection) } + +// Testing - no database needed! +run getUserCount() with { Database = mockDb([Row { count: 42 }]) } +``` + +## Summary + +| Pattern | Use Case | Resume? | +|---------|----------|---------| +| Basic | Provide implementation | Yes | +| Early return | Validation, errors | No | +| Reader | Configuration | Yes | +| Writer | Logging, accumulation | Yes | +| State | Counters, caches | Yes | +| Exception | Error handling | Sometimes | + +## Next + +[Chapter 7: Modules](07-modules.md) - Organizing code across files. diff --git a/docs/guide/07-modules.md b/docs/guide/07-modules.md new file mode 100644 index 0000000..d2572c4 --- /dev/null +++ b/docs/guide/07-modules.md @@ -0,0 +1,320 @@ +# Chapter 7: Modules + +As programs grow, you need to split code across files. Lux has a module system for organizing and sharing code. + +## Module Basics + +Every `.lux` file is a module. The file path determines the module path: + +``` +project/ +├── main.lux # Module: main +├── utils.lux # Module: utils +└── lib/ + ├── math.lux # Module: lib/math + └── strings.lux # Module: lib/strings +``` + +## Importing Modules + +### Basic Import + +```lux +import lib/math + +fn main(): Unit with {Console} = { + Console.print(toString(lib/math.square(5))) +} +``` + +Wait, that's verbose. Use an alias: + +### Aliased Import + +```lux +import lib/math as math + +fn main(): Unit with {Console} = { + Console.print(toString(math.square(5))) +} +``` + +### Selective Import + +Import specific items directly: + +```lux +import lib/math.{square, cube} + +fn main(): Unit with {Console} = { + Console.print(toString(square(5))) // No prefix needed + Console.print(toString(cube(3))) +} +``` + +### Wildcard Import + +Import everything: + +```lux +import lib/math.* + +fn main(): Unit with {Console} = { + Console.print(toString(square(5))) + Console.print(toString(cube(3))) + Console.print(toString(factorial(6))) +} +``` + +Use sparingly—it can cause name conflicts. + +## Visibility + +By default, declarations are private. Use `pub` to export: + +```lux +// lib/math.lux + +// Public - can be imported +pub fn square(x: Int): Int = x * x + +pub fn cube(x: Int): Int = x * x * x + +// Private - internal helper +fn helper(x: Int): Int = x + 1 + +// Public type +pub type Point = { x: Int, y: Int } +``` + +## Creating a Module + +Let's create a string utilities module: + +```lux +// lib/strings.lux + +/// Repeat a string n times +pub fn repeat(s: String, n: Int): String = + if n <= 0 then "" + else s + repeat(s, n - 1) + +/// Check if string starts with prefix +pub fn startsWith(s: String, prefix: String): Bool = + String.startsWith(s, prefix) + +/// Check if string ends with suffix +pub fn endsWith(s: String, suffix: String): Bool = + String.endsWith(s, suffix) + +/// Pad string on the left to reach target length +pub fn padLeft(s: String, length: Int, char: String): String = { + let current = String.length(s) + if current >= length then s + else padLeft(char + s, length, char) +} + +/// Pad string on the right to reach target length +pub fn padRight(s: String, length: Int, char: String): String = { + let current = String.length(s) + if current >= length then s + else padRight(s + char, length, char) +} +``` + +Using it: + +```lux +// main.lux +import lib/strings as str + +fn main(): Unit with {Console} = { + Console.print(str.repeat("ab", 3)) // "ababab" + Console.print(str.padLeft("5", 3, "0")) // "005" +} + +let output = run main() with {} +``` + +## Module Organization Patterns + +### Feature Modules + +Group by feature: + +``` +project/ +├── main.lux +├── users/ +│ ├── types.lux # User type definitions +│ ├── repository.lux # Database operations +│ └── service.lux # Business logic +├── orders/ +│ ├── types.lux +│ ├── repository.lux +│ └── service.lux +└── shared/ + ├── utils.lux + └── effects.lux +``` + +### Layer Modules + +Group by layer: + +``` +project/ +├── main.lux +├── domain/ # Business logic (pure) +│ ├── user.lux +│ └── order.lux +├── effects/ # Effect definitions +│ ├── database.lux +│ └── email.lux +├── handlers/ # Effect implementations +│ ├── postgres.lux +│ └── smtp.lux +└── api/ # Entry points + └── http.lux +``` + +## Standard Library + +Lux has a standard library in the `std/` directory: + +```lux +import std/prelude.* // Common utilities +import std/option // Option helpers +import std/result // Result helpers +import std/io // I/O utilities +``` + +### std/prelude + +```lux +import std/prelude.* + +identity(42) // 42 +compose(f, g) // Function composition +not(true) // false +and(true, false) // false +or(true, false) // true +``` + +### std/option + +```lux +import std/option as opt + +opt.some(42) // Some(42) +opt.none() // None +opt.map(Some(5), double) // Some(10) +opt.flatMap(Some(5), safeDivide) +opt.unwrapOr(None, 0) // 0 +``` + +### std/result + +```lux +import std/result as res + +res.ok(42) // Ok(42) +res.err("oops") // Err("oops") +res.mapOk(Ok(5), double) // Ok(10) +res.mapErr(Err("x"), upper) // Err("X") +``` + +## Circular Dependencies + +Lux detects circular imports: + +```lux +// a.lux +import b +pub fn fromA(): Int = b.fromB() + 1 + +// b.lux +import a +pub fn fromB(): Int = a.fromA() + 1 + +// Error: Circular dependency detected +``` + +Solution: extract shared code to a third module. + +## Module Best Practices + +### 1. One Concept Per Module + +```lux +// Good: focused module +// user.lux - User type and operations +pub type User = { id: Int, name: String } +pub fn createUser(name: String): User = ... +pub fn validateUser(u: User): Bool = ... + +// Bad: kitchen sink +// utils.lux - random stuff +pub fn parseUser(s: String): User = ... +pub fn formatDate(d: Date): String = ... +pub fn calculateTax(amount: Int): Int = ... +``` + +### 2. Export Deliberately + +```lux +// Only export what others need +pub fn publicApi(): Result = ... + +// Keep helpers private +fn internalHelper(): Int = ... +``` + +### 3. Use Aliases for Clarity + +```lux +// Clear what comes from where +import database/postgres as db +import cache/redis as cache + +fn getData(id: Int): Data with {Database, Cache} = { + match cache.get(id) { + Some(d) => d, + None => { + let d = db.query(id) + cache.set(id, d) + d + } + } +} +``` + +### 4. Group Related Imports + +```lux +// Standard library +import std/prelude.* +import std/option as opt + +// Project modules +import lib/database as db +import lib/cache as cache + +// Local modules +import ./types.{User, Order} +import ./validation +``` + +## Summary + +| Syntax | Meaning | +|--------|---------| +| `import path/to/module` | Import module | +| `import path/to/module as alias` | Import with alias | +| `import path/to/module.{a, b}` | Import specific items | +| `import path/to/module.*` | Import all exports | +| `pub fn` / `pub type` | Export declaration | + +## Next + +[Chapter 8: Error Handling](08-errors.md) - Handling failures gracefully. diff --git a/docs/guide/08-errors.md b/docs/guide/08-errors.md new file mode 100644 index 0000000..5ac0976 --- /dev/null +++ b/docs/guide/08-errors.md @@ -0,0 +1,294 @@ +# Chapter 8: Error Handling + +Errors happen. Lux provides multiple ways to handle them, all explicit in the type system. + +## The Fail Effect + +The simplest error handling uses the built-in `Fail` effect: + +```lux +fn divide(a: Int, b: Int): Int with {Fail} = + if b == 0 then Fail.fail("Division by zero") + else a / b + +fn main(): Unit with {Console} = { + let result = run divide(10, 2) with {} + Console.print("Result: " + toString(result)) +} +``` + +When `b == 0`, `Fail.fail` stops execution. The program terminates with an error. + +### Fail Propagates + +```lux +fn helper(): Int with {Fail} = + Fail.fail("Oops") + +fn caller(): Int with {Fail} = { + let x = helper() // Fails here + x * 2 // Never reached +} +``` + +If you call a function with `Fail`, you must declare it: + +```lux +// Error: caller uses Fail but doesn't declare it +fn broken(): Int = { + divide(10, 0) +} +``` + +## Option - Maybe There's a Value + +For operations that might not have a result: + +```lux +fn safeDivide(a: Int, b: Int): Option = + if b == 0 then None + else Some(a / b) + +fn findUser(id: Int): Option = + // Returns None if user doesn't exist + Database.query("SELECT * FROM users WHERE id = " + toString(id)) +``` + +### Working with Option + +**Pattern matching:** +```lux +fn showResult(opt: Option): String = + match opt { + Some(n) => "Got: " + toString(n), + None => "No value" + } +``` + +**Option methods:** +```lux +let x = Some(5) +let y: Option = None + +Option.map(x, fn(n: Int): Int => n * 2) // Some(10) +Option.map(y, fn(n: Int): Int => n * 2) // None + +Option.getOrElse(x, 0) // 5 +Option.getOrElse(y, 0) // 0 + +Option.isSome(x) // true +Option.isNone(y) // true +``` + +**Chaining with flatMap:** +```lux +fn getUserName(id: Int): Option = + Option.flatMap(findUser(id), fn(user: User): Option => + Some(user.name) + ) +``` + +## Result - Success or Failure + +For operations that can fail with an error value: + +```lux +fn parseNumber(s: String): Result = + if isNumeric(s) then Ok(parseInt(s)) + else Err("Not a number: " + s) + +fn readConfig(path: String): Result with {File} = + if File.exists(path) then + match parseConfig(File.read(path)) { + Some(c) => Ok(c), + None => Err("Invalid config format") + } + else Err("Config file not found: " + path) +``` + +### Working with Result + +**Pattern matching:** +```lux +fn handleResult(r: Result): String = + match r { + Ok(n) => "Success: " + toString(n), + Err(e) => "Error: " + e + } +``` + +**Result methods:** +```lux +let success = Ok(42) +let failure: Result = Err("oops") + +Result.map(success, fn(n: Int): Int => n * 2) // Ok(84) +Result.map(failure, fn(n: Int): Int => n * 2) // Err("oops") + +Result.getOrElse(success, 0) // 42 +Result.getOrElse(failure, 0) // 0 + +Result.isOk(success) // true +Result.isErr(failure) // true +``` + +**Chaining operations:** +```lux +fn processData(input: String): Result = { + let parsed = parseInput(input) // Result + let validated = Result.flatMap(parsed, validate) // Result + Result.map(validated, transform) // Result +} +``` + +## Combining Approaches + +### Option to Result + +```lux +fn optionToResult(opt: Option, error: String): Result = + match opt { + Some(v) => Ok(v), + None => Err(error) + } + +fn findUserOrError(id: Int): Result = + optionToResult(findUser(id), "User not found: " + toString(id)) +``` + +### Result to Option + +```lux +fn resultToOption(r: Result): Option = + match r { + Ok(v) => Some(v), + Err(_) => None + } +``` + +### Fail with Result + +```lux +fn processOrFail(input: String): Output with {Fail} = + match process(input) { + Ok(output) => output, + Err(e) => Fail.fail(e) + } +``` + +## Error Handling Patterns + +### Early Return with Fail + +```lux +fn validateUser(user: User): User with {Fail} = { + if user.name == "" then Fail.fail("Name required") + else if user.age < 0 then Fail.fail("Invalid age") + else if user.email == "" then Fail.fail("Email required") + else user +} + +fn registerUser(user: User): UserId with {Fail, Database} = { + let validated = validateUser(user) + Database.insert(validated) +} +``` + +### Collecting Errors + +```lux +fn validateAll(user: User): Result> = { + let errors: List = [] + let errors = if user.name == "" then List.concat(errors, ["Name required"]) else errors + let errors = if user.age < 0 then List.concat(errors, ["Invalid age"]) else errors + let errors = if user.email == "" then List.concat(errors, ["Email required"]) else errors + + if List.isEmpty(errors) then Ok(user) + else Err(errors) +} +``` + +### Default Values + +```lux +fn getConfig(key: String): String = + Option.getOrElse(Config.get(key), "default") + +fn getPort(): Int = + Result.getOrElse(parseNumber(Process.env("PORT")), 8080) +``` + +### Logging Errors + +```lux +fn processWithLogging(input: String): Result with {Console} = { + let result = process(input) + match result { + Ok(_) => result, + Err(e) => { + Console.print("Error processing input: " + e) + result + } + } +} +``` + +## Custom Error Types + +Define specific error types: + +```lux +type ValidationError = + | MissingField(String) + | InvalidFormat(String, String) // field, expected format + | OutOfRange(String, Int, Int) // field, min, max + +fn validate(user: User): Result = { + if user.name == "" then Err(MissingField("name")) + else if user.age < 0 then Err(OutOfRange("age", 0, 150)) + else if !isValidEmail(user.email) then Err(InvalidFormat("email", "user@domain.com")) + else Ok(user) +} + +fn showError(e: ValidationError): String = + match e { + MissingField(f) => "Missing required field: " + f, + InvalidFormat(f, fmt) => f + " must be in format: " + fmt, + OutOfRange(f, min, max) => + f + " must be between " + toString(min) + " and " + toString(max) + } +``` + +## When to Use What + +| Scenario | Use | +|----------|-----| +| Might not exist | `Option` | +| Can fail with reason | `Result` | +| Fatal error, stop execution | `Fail` effect | +| Multiple error types | Custom error ADT | + +## Summary + +```lux +// Option - maybe a value +let opt: Option = Some(42) +let none: Option = None + +// Result - success or error +let ok: Result = Ok(42) +let err: Result = Err("failed") + +// Fail effect - abort execution +fn risky(): Int with {Fail} = Fail.fail("boom") + +// Pattern match to handle +match result { + Ok(v) => handleSuccess(v), + Err(e) => handleError(e) +} +``` + +## Next + +[Chapter 9: Standard Library](09-stdlib.md) - Built-in functions and modules. diff --git a/docs/guide/09-stdlib.md b/docs/guide/09-stdlib.md new file mode 100644 index 0000000..b85948e --- /dev/null +++ b/docs/guide/09-stdlib.md @@ -0,0 +1,318 @@ +# Chapter 9: Standard Library + +Lux comes with a comprehensive standard library. This chapter covers the built-in modules. + +## Built-in Functions + +Always available, no import needed: + +```lux +toString(42) // "42" - convert any value to string +typeOf(42) // "Int" - get type name +print("hello") // Print to console (shortcut) +``` + +## List Module + +Operations on lists: + +```lux +let nums = [1, 2, 3, 4, 5] + +// Transformations +List.map(nums, fn(x: Int): Int => x * 2) // [2, 4, 6, 8, 10] +List.filter(nums, fn(x: Int): Bool => x > 2) // [3, 4, 5] +List.fold(nums, 0, fn(acc: Int, x: Int): Int => acc + x) // 15 + +// Access +List.head(nums) // Some(1) +List.tail(nums) // [2, 3, 4, 5] +List.get(nums, 2) // Some(3) +List.length(nums) // 5 +List.isEmpty([]) // true + +// Building +List.range(1, 5) // [1, 2, 3, 4] +List.concat([1,2], [3,4]) // [1, 2, 3, 4] +List.reverse(nums) // [5, 4, 3, 2, 1] + +// Searching +List.find(nums, fn(x: Int): Bool => x > 3) // Some(4) +List.any(nums, fn(x: Int): Bool => x > 3) // true +List.all(nums, fn(x: Int): Bool => x > 0) // true + +// Slicing +List.take(nums, 3) // [1, 2, 3] +List.drop(nums, 3) // [4, 5] +``` + +## String Module + +String manipulation: + +```lux +let s = "Hello, World!" + +// Info +String.length(s) // 13 +String.isEmpty("") // true + +// Search +String.contains(s, "World") // true +String.startsWith(s, "Hello") // true +String.endsWith(s, "!") // true + +// Transform +String.toUpper(s) // "HELLO, WORLD!" +String.toLower(s) // "hello, world!" +String.trim(" hi ") // "hi" +String.replace(s, "World", "Lux") // "Hello, Lux!" + +// Split/Join +String.split("a,b,c", ",") // ["a", "b", "c"] +String.join(["a","b","c"], "-") // "a-b-c" +String.lines("a\nb\nc") // ["a", "b", "c"] +String.chars("abc") // ["a", "b", "c"] + +// Substring +String.substring(s, 0, 5) // "Hello" +``` + +## Option Module + +Working with optional values: + +```lux +let some = Some(42) +let none: Option = None + +// Check +Option.isSome(some) // true +Option.isNone(none) // true + +// Transform +Option.map(some, fn(x: Int): Int => x * 2) // Some(84) +Option.flatMap(some, fn(x: Int): Option => Some(x + 1)) // Some(43) + +// Extract +Option.getOrElse(some, 0) // 42 +Option.getOrElse(none, 0) // 0 +``` + +## Result Module + +Working with results: + +```lux +let ok: Result = Ok(42) +let err: Result = Err("oops") + +// Check +Result.isOk(ok) // true +Result.isErr(err) // true + +// Transform +Result.map(ok, fn(x: Int): Int => x * 2) // Ok(84) +Result.mapErr(err, fn(e: String): String => "Error: " + e) // Err("Error: oops") +Result.flatMap(ok, fn(x: Int): Result => Ok(x + 1)) // Ok(43) + +// Extract +Result.getOrElse(ok, 0) // 42 +Result.getOrElse(err, 0) // 0 +``` + +## Math Module + +Mathematical functions: + +```lux +Math.abs(-5) // 5 +Math.min(3, 7) // 3 +Math.max(3, 7) // 7 +Math.pow(2, 10) // 1024 +Math.sqrt(16) // 4.0 +Math.floor(3.7) // 3 +Math.ceil(3.2) // 4 +Math.round(3.5) // 4 +``` + +## Json Module + +JSON parsing and generation: + +```lux +// Parse JSON string +let data = Json.parse("{\"name\": \"Alice\", \"age\": 30}") + +// Access fields +Json.get(data, "name") // Some("Alice") +Json.getInt(data, "age") // Some(30) + +// Create JSON +let obj = Json.object([ + ("name", Json.string("Bob")), + ("age", Json.int(25)) +]) + +// Convert to string +Json.stringify(obj) // "{\"name\":\"Bob\",\"age\":25}" +Json.prettyPrint(obj) // Formatted with indentation +``` + +## Standard Library Modules (std/) + +Import from the `std/` directory: + +### std/prelude + +```lux +import std/prelude.* + +identity(42) // 42 +compose(f, g) // Function composition +flip(f) // Flip argument order +not(true) // false +and(true, false) // false +or(true, false) // true +``` + +### std/io + +```lux +import std/io + +io.println("Hello") // Print with newline +io.print("No newline") // Print without newline +io.readLine() // Read line from input +io.debug("label", value) // Debug print, returns value +``` + +### std/option + +```lux +import std/option as opt + +opt.some(42) // Some(42) +opt.none() // None +opt.map(x, f) // Map function over option +opt.flatMap(x, f) // FlatMap +opt.filter(x, pred) // Filter by predicate +opt.toList(x) // Convert to list +``` + +### std/result + +```lux +import std/result as res + +res.ok(42) // Ok(42) +res.err("oops") // Err("oops") +res.mapOk(r, f) // Map over Ok value +res.mapErr(r, f) // Map over Err value +res.unwrapOr(r, default) // Get value or default +``` + +## Built-in Effects + +### Console + +```lux +fn example(): Unit with {Console} = { + Console.print("Hello") // Print string + let line = Console.readLine() // Read line + let num = Console.readInt() // Read integer +} +``` + +### File + +```lux +fn example(): Unit with {File} = { + let content = File.read("file.txt") // Read file + File.write("out.txt", "content") // Write file + let exists = File.exists("file.txt") // Check existence + let files = File.list("./dir") // List directory + File.mkdir("newdir") // Create directory + File.delete("file.txt") // Delete file +} +``` + +### Process + +```lux +fn example(): Unit with {Process} = { + let output = Process.exec("ls", ["-la"]) // Run command + let home = Process.env("HOME") // Get env var + let args = Process.args() // Get CLI args + let cwd = Process.cwd() // Current directory + Process.exit(0) // Exit program +} +``` + +### Http + +```lux +fn example(): Unit with {Http} = { + let body = Http.get("https://api.example.com/data") + let response = Http.post("https://api.example.com/data", jsonBody) + Http.put("https://api.example.com/data/1", updatedBody) + Http.delete("https://api.example.com/data/1") +} +``` + +### Random + +```lux +fn example(): Unit with {Random} = { + let n = Random.int(1, 100) // Random int in range + let f = Random.float() // Random float 0.0-1.0 + let b = Random.bool() // Random boolean +} +``` + +### State + +```lux +fn example(): Int with {State} = { + let current = State.get() // Get current state + State.put(current + 1) // Update state + State.get() +} + +// Initialize with value +run example() with { State = 0 } +``` + +### Fail + +```lux +fn example(): Int with {Fail} = { + if condition then Fail.fail("Error message") + else 42 +} +``` + +## Quick Reference + +| Module | Key Functions | +|--------|---------------| +| List | map, filter, fold, head, tail, length, concat | +| String | split, join, trim, contains, replace, toUpper | +| Option | map, flatMap, getOrElse, isSome, isNone | +| Result | map, mapErr, flatMap, getOrElse, isOk | +| Math | abs, min, max, pow, sqrt, floor, ceil | +| Json | parse, stringify, get, object, array | + +| Effect | Operations | +|--------|------------| +| Console | print, readLine, readInt | +| File | read, write, exists, list, mkdir, delete | +| Process | exec, env, args, cwd, exit | +| Http | get, post, put, delete | +| Random | int, float, bool | +| State | get, put | +| Fail | fail | + +## Next + +[Chapter 10: Advanced Topics](10-advanced.md) - Traits, generics, and optimization. diff --git a/docs/guide/10-advanced.md b/docs/guide/10-advanced.md new file mode 100644 index 0000000..f3dad73 --- /dev/null +++ b/docs/guide/10-advanced.md @@ -0,0 +1,325 @@ +# Chapter 10: Advanced Topics + +This chapter covers advanced features for building larger applications. + +## Traits + +Traits define shared behavior across types: + +```lux +trait Show { + fn show(self): String +} + +impl Show for Int { + fn show(self): String = toString(self) +} + +impl Show for Bool { + fn show(self): String = if self then "true" else "false" +} + +impl Show for List where T: Show { + fn show(self): String = { + let items = List.map(self, fn(x: T): String => x.show()) + "[" + String.join(items, ", ") + "]" + } +} +``` + +Using traits: + +```lux +fn display(value: T): Unit with {Console} where T: Show = + Console.print(value.show()) + +display(42) // "42" +display(true) // "true" +display([1, 2, 3]) // "[1, 2, 3]" +``` + +## Generic Types + +Types with parameters: + +```lux +type Pair = + | MkPair(A, B) + +fn first(p: Pair): A = + match p { + MkPair(a, _) => a + } + +fn second(p: Pair): B = + match p { + MkPair(_, b) => b + } + +let p = MkPair(1, "one") +first(p) // 1 +second(p) // "one" +``` + +## Type Constraints + +Restrict generic types: + +```lux +fn maximum(list: List): Option where T: Ord = { + match list { + [] => None, + [x] => Some(x), + [x, ...rest] => { + match maximum(rest) { + None => Some(x), + Some(y) => Some(if x > y then x else y) + } + } + } +} +``` + +## Tail Call Optimization + +Lux optimizes tail-recursive functions: + +```lux +// Not tail-recursive - stack grows with each call +fn sumBad(n: Int): Int = + if n <= 0 then 0 + else n + sumBad(n - 1) // Addition happens AFTER recursive call + +// Tail-recursive - constant stack space +fn sumGood(n: Int, acc: Int): Int = + if n <= 0 then acc + else sumGood(n - 1, acc + n) // Recursive call is the LAST operation + +fn sum(n: Int): Int = sumGood(n, 0) +``` + +The compiler transforms tail calls into loops, preventing stack overflow. + +## Effect Polymorphism + +Functions can be polymorphic over effects: + +```lux +fn withLogging(action: fn(): Int with {E}): Int with {E, Console} = { + Console.print("Starting action") + let result = action() + Console.print("Action returned: " + toString(result)) + result +} + +// Works with any effect set +fn pureAction(): Int = 42 +fn randomAction(): Int with {Random} = Random.int(1, 100) + +withLogging(pureAction) // Works +withLogging(randomAction) // Works +``` + +## Behavioral Properties + +Annotate functions with properties: + +```lux +// Pure function - no effects +fn add(a: Int, b: Int): Int is pure = a + b + +// Total function - always terminates +fn factorial(n: Int): Int is total = + if n <= 1 then 1 else n * factorial(n - 1) + +// Idempotent - same result if called multiple times +fn setConfig(key: String, value: String): Unit with {State} is idempotent = + State.put(value) +``` + +These are currently documentation, but future versions may verify them. + +## Documentation Comments + +Use `///` for documentation: + +```lux +/// Calculates the factorial of a non-negative integer. +/// +/// # Arguments +/// * `n` - A non-negative integer +/// +/// # Returns +/// The factorial of n (n!) +/// +/// # Example +/// ``` +/// factorial(5) // Returns 120 +/// ``` +pub fn factorial(n: Int): Int = + if n <= 1 then 1 else n * factorial(n - 1) +``` + +## Performance Tips + +### 1. Use Tail Recursion + +```lux +// Slow - builds up stack +fn lengthSlow(list: List): Int = + match list { + [] => 0, + [_, ...rest] => 1 + lengthSlow(rest) + } + +// Fast - constant stack +fn lengthFast(list: List, acc: Int): Int = + match list { + [] => acc, + [_, ...rest] => lengthFast(rest, acc + 1) + } +``` + +### 2. Avoid Repeated Concatenation + +```lux +// Slow - O(n²) +fn buildStringSlow(n: Int): String = + if n <= 0 then "" + else buildStringSlow(n - 1) + "x" + +// Fast - use List.join +fn buildStringFast(n: Int): String = + String.join(List.map(List.range(0, n), fn(_: Int): String => "x"), "") +``` + +### 3. Use Built-in Functions + +```lux +// Slow - manual implementation +fn sumManual(nums: List): Int = + match nums { + [] => 0, + [x, ...rest] => x + sumManual(rest) + } + +// Fast - built-in fold +fn sumBuiltin(nums: List): Int = + List.fold(nums, 0, fn(acc: Int, x: Int): Int => acc + x) +``` + +### 4. JIT Compilation + +For performance-critical numeric code, the JIT compiler provides ~160x speedup: + +```lux +// In Rust code, use the JIT compiler +let mut jit = JitCompiler::new().unwrap(); +jit.compile_function(&func).unwrap(); +let result = unsafe { jit.call_function("fib", &[30]).unwrap() }; +``` + +## Debugging + +### Debug Printing + +```lux +fn debug(label: String, value: T): T with {Console} = { + Console.print(label + ": " + toString(value)) + value +} + +fn process(x: Int): Int with {Console} = { + let step1 = debug("step1", x * 2) + let step2 = debug("step2", step1 + 10) + step2 +} +``` + +### The Debugger + +Run with debugger: + +```bash +lux --debug program.lux +``` + +Commands: +- `step` / `s` - Step into +- `next` / `n` - Step over +- `continue` / `c` - Continue +- `print ` - Evaluate expression +- `break ` - Set breakpoint +- `quit` / `q` - Exit + +### Effect Tracing + +```lux +fn traced(action: fn(): T with {E}): T with {E, Console} = { + Console.print(">>> Entering action") + let result = action() + Console.print("<<< Exiting with: " + toString(result)) + result +} +``` + +## IDE Support + +Lux has LSP support for: +- **VS Code**: Install Lux extension +- **Neovim**: Configure with nvim-lspconfig + +Features: +- Syntax highlighting +- Error diagnostics +- Go to definition +- Hover for types +- Auto-completion + +Start LSP server: +```bash +lux --lsp +``` + +## Project Structure + +Recommended layout for larger projects: + +``` +my-project/ +├── lux.toml # Project manifest +├── src/ +│ ├── main.lux # Entry point +│ ├── lib.lux # Library code +│ └── modules/ +│ ├── users.lux +│ └── orders.lux +├── std/ # Custom std extensions +├── tests/ +│ ├── users_test.lux +│ └── orders_test.lux +└── examples/ + └── demo.lux +``` + +## Summary + +| Feature | Syntax | +|---------|--------| +| Trait | `trait Name { fn method(self): T }` | +| Impl | `impl Trait for Type { ... }` | +| Generic | `fn f(x: T): T` | +| Constraint | `where T: Trait` | +| Tail recursion | Last expression is recursive call | +| Doc comment | `/// Documentation` | + +## What's Next? + +You now know Lux! Try: + +1. **Build something**: See [Tutorials](../tutorials/README.md) +2. **Read the reference**: See [Language Reference](../reference/syntax.md) +3. **Explore effects**: See [Effects Cookbook](../tutorials/effects-cookbook.md) +4. **Join the community**: GitHub discussions + +Happy coding with Lux! diff --git a/docs/reference/syntax.md b/docs/reference/syntax.md new file mode 100644 index 0000000..d7a334e --- /dev/null +++ b/docs/reference/syntax.md @@ -0,0 +1,509 @@ +# Language Reference: Syntax + +Complete syntax reference for Lux. + +## Lexical Structure + +### Comments + +```lux +// Single-line comment + +/* Multi-line + comment */ + +/// Documentation comment (for declarations) +``` + +### Identifiers + +``` +identifier = letter (letter | digit | '_')* +type_name = upper_letter (letter | digit)* +``` + +Examples: `foo`, `myVar`, `Type`, `Option` + +### Literals + +```lux +// Integers +42 +-17 +1_000_000 // Underscores for readability +0xFF // Hexadecimal +0b1010 // Binary + +// Floats +3.14 +2.5e10 +-1.5e-3 + +// Strings +"hello" +"line1\nline2" // Escape sequences +"value: ${expression}" // String interpolation +"multi +line +string" + +// Characters +'a' +'\n' +'🎉' + +// Booleans +true +false + +// Unit +() + +// Lists +[] +[1, 2, 3] +["a", "b", "c"] + +// Records +{} +{ x: 1, y: 2 } +{ name: "Alice", age: 30 } +``` + +### Escape Sequences + +| Sequence | Meaning | +|----------|---------| +| `\n` | Newline | +| `\t` | Tab | +| `\r` | Carriage return | +| `\\` | Backslash | +| `\"` | Double quote | +| `\'` | Single quote | +| `\$` | Dollar sign | + +## Declarations + +### Functions + +```lux +// Basic function +fn name(param: Type): ReturnType = body + +// Multiple parameters +fn add(a: Int, b: Int): Int = a + b + +// Block body +fn complex(x: Int): Int = { + let y = x * 2 + y + 1 +} + +// With effects +fn greet(name: String): Unit with {Console} = + Console.print("Hello, " + name) + +// With type parameters +fn identity(x: T): T = x + +// With constraints +fn show(x: T): String where T: Show = x.show() + +// With behavioral properties +fn pure_add(a: Int, b: Int): Int is pure = a + b + +// With visibility +pub fn exported(): Int = 42 + +// With documentation +/// Computes the factorial of n +pub fn factorial(n: Int): Int = + if n <= 1 then 1 else n * factorial(n - 1) +``` + +### Types + +```lux +// Type alias +type UserId = Int + +// Enum (sum type) +type Color = + | Red + | Green + | Blue + +// With data +type Option = + | Some(T) + | None + +// With named fields +type Person = + | Person { name: String, age: Int } + +// Record type +type Point = { x: Int, y: Int } + +// With visibility +pub type PublicType = ... +``` + +### Effects + +```lux +effect EffectName { + fn operation1(param: Type): ReturnType + fn operation2(): Type +} + +// Example +effect Logger { + fn log(level: String, message: String): Unit + fn getLevel(): String +} +``` + +### Handlers + +```lux +handler handlerName: EffectName { + fn operation1(param) = { + // implementation + resume(value) + } +} + +// Example +handler consoleLogger: Logger { + fn log(level, msg) = { + Console.print("[" + level + "] " + msg) + resume(()) + } + fn getLevel() = resume("debug") +} +``` + +### Traits + +```lux +trait TraitName { + fn method(self): ReturnType + fn method2(self, param: Type): ReturnType +} + +impl TraitName for Type { + fn method(self): ReturnType = ... + fn method2(self, param: Type): ReturnType = ... +} +``` + +### Let Bindings + +```lux +// Top-level +let name = value +let name: Type = value +pub let exported = value + +// Local (in blocks) +let x = 42 +let (a, b) = (1, 2) +``` + +## Expressions + +### Literals + +See Lexical Structure above. + +### Variables + +```lux +x +myVariable +Some +``` + +### Function Application + +```lux +f(x) +f(x, y, z) +moduleName.function(arg) +``` + +### Operators + +```lux +// Arithmetic +a + b // Addition +a - b // Subtraction +a * b // Multiplication +a / b // Division +a % b // Remainder +-a // Negation + +// Comparison +a == b // Equal +a != b // Not equal +a < b // Less than +a > b // Greater than +a <= b // Less or equal +a >= b // Greater or equal + +// Boolean +a && b // And +a || b // Or +!a // Not + +// String +s1 + s2 // Concatenation +``` + +### Conditionals + +```lux +if condition then expr1 else expr2 + +if condition then + expr1 +else + expr2 + +// Chained +if c1 then e1 +else if c2 then e2 +else e3 +``` + +### Match Expressions + +```lux +match value { + pattern1 => expr1, + pattern2 => expr2, + _ => default +} + +// With guards +match value { + n if n > 0 => "positive", + n if n < 0 => "negative", + _ => "zero" +} + +// Nested patterns +match expr { + Add(Num(0), x) => x, + Mul(x, Num(1)) => x, + _ => expr +} +``` + +### Blocks + +```lux +{ + statement1 + statement2 + result_expression +} + +// Statements can be: +// - let bindings +// - expressions (for side effects) +``` + +### Lambda Expressions + +```lux +fn(x: Type): ReturnType => body + +// Examples +fn(x: Int): Int => x * 2 +fn(a: Int, b: Int): Int => a + b +fn(): Unit => Console.print("hi") +``` + +### Run Expressions + +```lux +run expr with {} + +run expr with { + Effect1 = handler1, + Effect2 = handler2 +} + +// Example +run computation() with { + Logger = consoleLogger, + Database = mockDb +} +``` + +### Field Access + +```lux +record.field +module.function +``` + +### Tuple Construction + +```lux +(a, b) +(x, y, z) +``` + +### Record Construction + +```lux +{ field1: value1, field2: value2 } + +// Update +{ ...existing, field: newValue } +``` + +### List Construction + +```lux +[] +[1, 2, 3] +[head, ...tail] +``` + +## Patterns + +Used in match expressions and let bindings: + +```lux +// Literal +42 +"hello" +true + +// Variable (binds the value) +x +name + +// Wildcard (matches anything) +_ + +// Constructor +Some(x) +None +Pair(a, b) + +// Tuple +(x, y) +(a, b, c) + +// List +[] +[x] +[x, y] +[head, ...tail] + +// Record +{ name, age } +{ name: n, age: a } + +// With guard +x if x > 0 +``` + +## Type Expressions + +```lux +// Primitive +Int +Float +Bool +String +Char +Unit + +// Named +TypeName +ModuleName.TypeName + +// Generic application +Option +Result +List + +// Function +fn(Int): Int +fn(Int, String): Bool +fn(T): T with {Effect} + +// Tuple +(Int, String) +(A, B, C) + +// Record +{ x: Int, y: Int } +{ name: String, age: Int } + +// List +List +``` + +## Modules + +### Imports + +```lux +// Full module +import path/to/module + +// With alias +import path/to/module as alias + +// Selective +import path/to/module.{item1, item2} + +// Wildcard +import path/to/module.* +``` + +### Exports + +```lux +// Public declarations +pub fn publicFunction(): Int = ... +pub type PublicType = ... +pub let publicValue = ... + +// Private (default) +fn privateFunction(): Int = ... +``` + +## Grammar Summary + +``` +program = import* declaration* + +import = 'import' path ('as' ident)? + | 'import' path '.{' ident (',' ident)* '}' + | 'import' path '.*' + +declaration = function | type | effect | handler | trait | impl | let + +function = 'pub'? 'fn' ident type_params? '(' params ')' ':' type + ('with' '{' effects '}')? ('is' properties)? + ('where' constraints)? '=' expr + +type = 'pub'? 'type' ident type_params? '=' type_def + +effect = 'effect' ident '{' operation* '}' + +handler = 'handler' ident ':' ident '{' impl* '}' + +expr = literal | ident | application | binary | unary + | if | match | block | lambda | run | field + +pattern = literal | ident | '_' | constructor | tuple | list | record +``` diff --git a/docs/tutorials/README.md b/docs/tutorials/README.md new file mode 100644 index 0000000..a2eaebb --- /dev/null +++ b/docs/tutorials/README.md @@ -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! diff --git a/docs/tutorials/calculator.md b/docs/tutorials/calculator.md new file mode 100644 index 0000000..ef19f81 --- /dev/null +++ b/docs/tutorials/calculator.md @@ -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 = + 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 = + 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 = { + 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 + fn set(name: String, value: Int): Unit +} + +fn evalWithVars(e: Expr): Result 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. diff --git a/docs/tutorials/dependency-injection.md b/docs/tutorials/dependency-injection.md new file mode 100644 index 0000000..135b158 --- /dev/null +++ b/docs/tutorials/dependency-injection.md @@ -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 +} + +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. diff --git a/docs/tutorials/project-ideas.md b/docs/tutorials/project-ideas.md new file mode 100644 index 0000000..7578bcb --- /dev/null +++ b/docs/tutorials/project-ideas.md @@ -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, 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 + fn list(): List + 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) + | Link(String, String) // text, url + +fn parseMarkdown(input: String): List = ... +fn toHtml(nodes: List): 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(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): Unit with {Runtime} = ... +``` + +### 12. Task Scheduler +Schedule and run tasks with dependencies. + +**Skills**: Graphs, effects, async simulation + +```lux +type Task = { id: String, deps: List, 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): 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 + fn await(future: Future): T + fn sleep(ms: Int): Unit +} + +fn parallel(): List 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!