docs: add comprehensive language documentation
Documentation structure inspired by Rust Book, Elm Guide, and others: Guide (10 chapters): - Introduction and setup - Basic types (Int, String, Bool, List, Option, Result) - Functions (closures, higher-order, composition) - Data types (ADTs, pattern matching, records) - Effects (the core innovation) - Handlers (patterns and techniques) - Modules (imports, exports, organization) - Error handling (Fail, Option, Result) - Standard library reference - Advanced topics (traits, generics, optimization) Reference: - Complete syntax reference Tutorials: - Calculator (parsing, evaluation, REPL) - Dependency injection (testing with effects) - Project ideas (16 projects by difficulty) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
227
docs/README.md
Normal file
227
docs/README.md
Normal file
@@ -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
|
||||
<function>
|
||||
|
||||
> 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.*
|
||||
222
docs/guide/01-introduction.md
Normal file
222
docs/guide/01-introduction.md
Normal file
@@ -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
|
||||
<function>
|
||||
|
||||
> square(5)
|
||||
25
|
||||
|
||||
> fn greet(name: String): Unit with {Console} = Console.print("Hi, " + name)
|
||||
<function>
|
||||
|
||||
> 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
|
||||
248
docs/guide/02-basic-types.md
Normal file
248
docs/guide/02-basic-types.md
Normal file
@@ -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<Int> = []
|
||||
```
|
||||
|
||||
## Lists
|
||||
|
||||
Ordered collections of the same type:
|
||||
|
||||
```lux
|
||||
let numbers = [1, 2, 3, 4, 5]
|
||||
let words = ["hello", "world"]
|
||||
let empty: List<Int> = []
|
||||
|
||||
// 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<Int> = Some(42)
|
||||
let no_value: Option<Int> = None
|
||||
|
||||
// Pattern matching
|
||||
fn describe(opt: Option<Int>): 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<Int, String> = Ok(42)
|
||||
let failure: Result<Int, String> = Err("Something went wrong")
|
||||
|
||||
// Pattern matching
|
||||
fn handle(r: Result<Int, String>): 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<T>` | `[1, 2, 3]` | Ordered collection |
|
||||
| `Option<T>` | `Some(42)` | Optional value |
|
||||
| `Result<T, E>` | `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.
|
||||
272
docs/guide/03-functions.md
Normal file
272
docs/guide/03-functions.md
Normal file
@@ -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<A, B, C>(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<T>(x: T): T = x
|
||||
|
||||
identity(42) // 42
|
||||
identity("hello") // "hello"
|
||||
identity(true) // true
|
||||
|
||||
fn pair<A, B>(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<T>(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.
|
||||
331
docs/guide/04-data-types.md
Normal file
331
docs/guide/04-data-types.md
Normal file
@@ -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<T>
|
||||
|
||||
For optional values:
|
||||
|
||||
```lux
|
||||
type Option<T> =
|
||||
| Some(T)
|
||||
| None
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```lux
|
||||
fn safeDivide(a: Int, b: Int): Option<Int> =
|
||||
if b == 0 then None
|
||||
else Some(a / b)
|
||||
|
||||
fn showResult(opt: Option<Int>): 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<T, E>
|
||||
|
||||
For operations that can fail with an error:
|
||||
|
||||
```lux
|
||||
type Result<T, E> =
|
||||
| Ok(T)
|
||||
| Err(E)
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```lux
|
||||
fn parseAge(s: String): Result<Int, String> =
|
||||
// Simplified - assume we have a real parser
|
||||
if s == "42" then Ok(42)
|
||||
else Err("Invalid age: " + s)
|
||||
|
||||
fn handleAge(r: Result<Int, String>): String =
|
||||
match r {
|
||||
Ok(age) => "Age is " + toString(age),
|
||||
Err(msg) => "Error: " + msg
|
||||
}
|
||||
```
|
||||
|
||||
### List<T>
|
||||
|
||||
Lists are built-in but conceptually:
|
||||
|
||||
```lux
|
||||
type List<T> =
|
||||
| Nil
|
||||
| Cons(T, List<T>)
|
||||
```
|
||||
|
||||
Pattern match on lists:
|
||||
|
||||
```lux
|
||||
fn sum(nums: List<Int>): Int =
|
||||
match nums {
|
||||
[] => 0,
|
||||
[x, ...rest] => x + sum(rest)
|
||||
}
|
||||
|
||||
fn length<T>(list: List<T>): Int =
|
||||
match list {
|
||||
[] => 0,
|
||||
[_, ...rest] => 1 + length(rest)
|
||||
}
|
||||
```
|
||||
|
||||
## Recursive Types
|
||||
|
||||
Types can reference themselves:
|
||||
|
||||
```lux
|
||||
type Tree<T> =
|
||||
| Leaf(T)
|
||||
| Node(Tree<T>, Tree<T>)
|
||||
|
||||
fn sumTree(t: Tree<Int>): 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<Int, String> =
|
||||
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.
|
||||
350
docs/guide/05-effects.md
Normal file
350
docs/guide/05-effects.md
Normal file
@@ -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<Item>): 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<Row>
|
||||
fn execute(sql: String): Int
|
||||
}
|
||||
|
||||
fn getUsers(): List<User> 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.
|
||||
348
docs/guide/06-handlers.md
Normal file
348
docs/guide/06-handlers.md
Normal file
@@ -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<String, String>): 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<Int, String> = run riskyOperation() with { Fail = catchFail }
|
||||
```
|
||||
|
||||
### The Choice Pattern
|
||||
|
||||
Non-determinism:
|
||||
|
||||
```lux
|
||||
effect Choice {
|
||||
fn choose(options: List<T>): 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<T> => 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<Row>
|
||||
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<Row>): 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.
|
||||
320
docs/guide/07-modules.md
Normal file
320
docs/guide/07-modules.md
Normal file
@@ -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.
|
||||
294
docs/guide/08-errors.md
Normal file
294
docs/guide/08-errors.md
Normal file
@@ -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<T> - Maybe There's a Value
|
||||
|
||||
For operations that might not have a result:
|
||||
|
||||
```lux
|
||||
fn safeDivide(a: Int, b: Int): Option<Int> =
|
||||
if b == 0 then None
|
||||
else Some(a / b)
|
||||
|
||||
fn findUser(id: Int): Option<User> =
|
||||
// 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<Int>): String =
|
||||
match opt {
|
||||
Some(n) => "Got: " + toString(n),
|
||||
None => "No value"
|
||||
}
|
||||
```
|
||||
|
||||
**Option methods:**
|
||||
```lux
|
||||
let x = Some(5)
|
||||
let y: Option<Int> = 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<String> =
|
||||
Option.flatMap(findUser(id), fn(user: User): Option<String> =>
|
||||
Some(user.name)
|
||||
)
|
||||
```
|
||||
|
||||
## Result<T, E> - Success or Failure
|
||||
|
||||
For operations that can fail with an error value:
|
||||
|
||||
```lux
|
||||
fn parseNumber(s: String): Result<Int, String> =
|
||||
if isNumeric(s) then Ok(parseInt(s))
|
||||
else Err("Not a number: " + s)
|
||||
|
||||
fn readConfig(path: String): Result<Config, String> 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<Int, String>): String =
|
||||
match r {
|
||||
Ok(n) => "Success: " + toString(n),
|
||||
Err(e) => "Error: " + e
|
||||
}
|
||||
```
|
||||
|
||||
**Result methods:**
|
||||
```lux
|
||||
let success = Ok(42)
|
||||
let failure: Result<Int, String> = 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<Output, String> = {
|
||||
let parsed = parseInput(input) // Result<Input, String>
|
||||
let validated = Result.flatMap(parsed, validate) // Result<Input, String>
|
||||
Result.map(validated, transform) // Result<Output, String>
|
||||
}
|
||||
```
|
||||
|
||||
## Combining Approaches
|
||||
|
||||
### Option to Result
|
||||
|
||||
```lux
|
||||
fn optionToResult<T>(opt: Option<T>, error: String): Result<T, String> =
|
||||
match opt {
|
||||
Some(v) => Ok(v),
|
||||
None => Err(error)
|
||||
}
|
||||
|
||||
fn findUserOrError(id: Int): Result<User, String> =
|
||||
optionToResult(findUser(id), "User not found: " + toString(id))
|
||||
```
|
||||
|
||||
### Result to Option
|
||||
|
||||
```lux
|
||||
fn resultToOption<T, E>(r: Result<T, E>): Option<T> =
|
||||
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<User, List<String>> = {
|
||||
let errors: List<String> = []
|
||||
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<Output, String> 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<User, ValidationError> = {
|
||||
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<T>` |
|
||||
| Can fail with reason | `Result<T, E>` |
|
||||
| Fatal error, stop execution | `Fail` effect |
|
||||
| Multiple error types | Custom error ADT |
|
||||
|
||||
## Summary
|
||||
|
||||
```lux
|
||||
// Option - maybe a value
|
||||
let opt: Option<Int> = Some(42)
|
||||
let none: Option<Int> = None
|
||||
|
||||
// Result - success or error
|
||||
let ok: Result<Int, String> = Ok(42)
|
||||
let err: Result<Int, String> = 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.
|
||||
318
docs/guide/09-stdlib.md
Normal file
318
docs/guide/09-stdlib.md
Normal file
@@ -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<Int> = 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<Int> => 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<Int, String> = Ok(42)
|
||||
let err: Result<Int, String> = 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<Int, String> => 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.
|
||||
325
docs/guide/10-advanced.md
Normal file
325
docs/guide/10-advanced.md
Normal file
@@ -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<T> 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<T>(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<A, B> =
|
||||
| MkPair(A, B)
|
||||
|
||||
fn first<A, B>(p: Pair<A, B>): A =
|
||||
match p {
|
||||
MkPair(a, _) => a
|
||||
}
|
||||
|
||||
fn second<A, B>(p: Pair<A, B>): 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<T>(list: List<T>): Option<T> 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<E>(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<T>(list: List<T>): Int =
|
||||
match list {
|
||||
[] => 0,
|
||||
[_, ...rest] => 1 + lengthSlow(rest)
|
||||
}
|
||||
|
||||
// Fast - constant stack
|
||||
fn lengthFast<T>(list: List<T>, 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>): Int =
|
||||
match nums {
|
||||
[] => 0,
|
||||
[x, ...rest] => x + sumManual(rest)
|
||||
}
|
||||
|
||||
// Fast - built-in fold
|
||||
fn sumBuiltin(nums: List<Int>): 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<T>(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 <expr>` - Evaluate expression
|
||||
- `break <line>` - Set breakpoint
|
||||
- `quit` / `q` - Exit
|
||||
|
||||
### Effect Tracing
|
||||
|
||||
```lux
|
||||
fn traced<E>(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<T>(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!
|
||||
509
docs/reference/syntax.md
Normal file
509
docs/reference/syntax.md
Normal file
@@ -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<T>(x: T): T = x
|
||||
|
||||
// With constraints
|
||||
fn show<T>(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<T> =
|
||||
| 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<Int>
|
||||
Result<String, Error>
|
||||
List<T>
|
||||
|
||||
// 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<Int>
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
125
docs/tutorials/README.md
Normal file
125
docs/tutorials/README.md
Normal file
@@ -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!
|
||||
251
docs/tutorials/calculator.md
Normal file
251
docs/tutorials/calculator.md
Normal file
@@ -0,0 +1,251 @@
|
||||
# Tutorial: Building a Calculator
|
||||
|
||||
Build a REPL calculator that evaluates arithmetic expressions.
|
||||
|
||||
## What You'll Learn
|
||||
|
||||
- Defining algebraic data types
|
||||
- Pattern matching
|
||||
- Recursive functions
|
||||
- REPL loops with effects
|
||||
|
||||
## The Goal
|
||||
|
||||
```
|
||||
Calculator REPL
|
||||
> 2 + 3
|
||||
5
|
||||
> (10 - 4) * 2
|
||||
12
|
||||
> 100 / 5 + 3
|
||||
23
|
||||
> quit
|
||||
Goodbye!
|
||||
```
|
||||
|
||||
## Step 1: Define the Expression Type
|
||||
|
||||
First, we model arithmetic expressions:
|
||||
|
||||
```lux
|
||||
// calculator.lux
|
||||
|
||||
type Expr =
|
||||
| Num(Int)
|
||||
| Add(Expr, Expr)
|
||||
| Sub(Expr, Expr)
|
||||
| Mul(Expr, Expr)
|
||||
| Div(Expr, Expr)
|
||||
```
|
||||
|
||||
This is an *algebraic data type* (ADT). An `Expr` is one of:
|
||||
- A number: `Num(42)`
|
||||
- An addition: `Add(Num(2), Num(3))`
|
||||
- And so on...
|
||||
|
||||
## Step 2: Evaluate Expressions
|
||||
|
||||
Now we evaluate expressions to integers:
|
||||
|
||||
```lux
|
||||
fn eval(e: Expr): Result<Int, String> =
|
||||
match e {
|
||||
Num(n) => Ok(n),
|
||||
Add(a, b) => evalBinOp(a, b, fn(x: Int, y: Int): Int => x + y),
|
||||
Sub(a, b) => evalBinOp(a, b, fn(x: Int, y: Int): Int => x - y),
|
||||
Mul(a, b) => evalBinOp(a, b, fn(x: Int, y: Int): Int => x * y),
|
||||
Div(a, b) => {
|
||||
match (eval(a), eval(b)) {
|
||||
(Ok(x), Ok(0)) => Err("Division by zero"),
|
||||
(Ok(x), Ok(y)) => Ok(x / y),
|
||||
(Err(e), _) => Err(e),
|
||||
(_, Err(e)) => Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn evalBinOp(a: Expr, b: Expr, op: fn(Int, Int): Int): Result<Int, String> =
|
||||
match (eval(a), eval(b)) {
|
||||
(Ok(x), Ok(y)) => Ok(op(x, y)),
|
||||
(Err(e), _) => Err(e),
|
||||
(_, Err(e)) => Err(e)
|
||||
}
|
||||
```
|
||||
|
||||
Pattern matching makes this clear:
|
||||
- `Num(n)` just returns the number
|
||||
- Operations evaluate both sides, then apply the operator
|
||||
- Division checks for zero
|
||||
|
||||
## Step 3: Parse Expressions
|
||||
|
||||
For simplicity, we'll parse a limited format. A real parser would be more complex.
|
||||
|
||||
```lux
|
||||
fn parseSimple(input: String): Result<Expr, String> = {
|
||||
let trimmed = String.trim(input)
|
||||
|
||||
// Try to parse as a number
|
||||
if isNumber(trimmed) then
|
||||
Ok(Num(parseInt(trimmed)))
|
||||
else
|
||||
// Try to find an operator
|
||||
match findOperator(trimmed) {
|
||||
Some((left, op, right)) => {
|
||||
match (parseSimple(left), parseSimple(right)) {
|
||||
(Ok(l), Ok(r)) => Ok(makeOp(l, op, r)),
|
||||
(Err(e), _) => Err(e),
|
||||
(_, Err(e)) => Err(e)
|
||||
}
|
||||
},
|
||||
None => Err("Cannot parse: " + trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
fn isNumber(s: String): Bool = {
|
||||
let chars = String.chars(s)
|
||||
List.all(chars, fn(c: String): Bool =>
|
||||
c == "-" || (c >= "0" && c <= "9")
|
||||
)
|
||||
}
|
||||
|
||||
fn parseInt(s: String): Int = {
|
||||
// Simplified - assumes valid integer
|
||||
List.fold(String.chars(s), 0, fn(acc: Int, c: String): Int =>
|
||||
if c == "-" then acc
|
||||
else acc * 10 + charToDigit(c)
|
||||
) * (if String.startsWith(s, "-") then -1 else 1)
|
||||
}
|
||||
|
||||
fn charToDigit(c: String): Int =
|
||||
match c {
|
||||
"0" => 0, "1" => 1, "2" => 2, "3" => 3, "4" => 4,
|
||||
"5" => 5, "6" => 6, "7" => 7, "8" => 8, "9" => 9,
|
||||
_ => 0
|
||||
}
|
||||
|
||||
fn findOperator(s: String): Option<(String, String, String)> = {
|
||||
// Find last +/- at depth 0 (lowest precedence)
|
||||
// Then *// (higher precedence)
|
||||
// This is a simplified approach
|
||||
let addSub = findOpAtDepth(s, ["+", "-"], 0)
|
||||
match addSub {
|
||||
Some(r) => Some(r),
|
||||
None => findOpAtDepth(s, ["*", "/"], 0)
|
||||
}
|
||||
}
|
||||
|
||||
fn makeOp(left: Expr, op: String, right: Expr): Expr =
|
||||
match op {
|
||||
"+" => Add(left, right),
|
||||
"-" => Sub(left, right),
|
||||
"*" => Mul(left, right),
|
||||
"/" => Div(left, right),
|
||||
_ => Num(0) // Should not happen
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4: The REPL Loop
|
||||
|
||||
Now we build the interactive loop:
|
||||
|
||||
```lux
|
||||
fn repl(): Unit with {Console} = {
|
||||
Console.print("Calculator REPL (type 'quit' to exit)")
|
||||
replLoop()
|
||||
}
|
||||
|
||||
fn replLoop(): Unit with {Console} = {
|
||||
Console.print("> ")
|
||||
let input = Console.readLine()
|
||||
|
||||
if input == "quit" then
|
||||
Console.print("Goodbye!")
|
||||
else {
|
||||
match parseSimple(input) {
|
||||
Ok(expr) => {
|
||||
match eval(expr) {
|
||||
Ok(result) => Console.print(toString(result)),
|
||||
Err(e) => Console.print("Error: " + e)
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("Parse error: " + e)
|
||||
}
|
||||
replLoop() // Continue the loop
|
||||
}
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = repl()
|
||||
|
||||
let output = run main() with {}
|
||||
```
|
||||
|
||||
## Step 5: Test It
|
||||
|
||||
```bash
|
||||
$ lux calculator.lux
|
||||
Calculator REPL (type 'quit' to exit)
|
||||
> 2 + 3
|
||||
5
|
||||
> 10 - 4
|
||||
6
|
||||
> 6 * 7
|
||||
42
|
||||
> 100 / 0
|
||||
Error: Division by zero
|
||||
> quit
|
||||
Goodbye!
|
||||
```
|
||||
|
||||
## Extending the Calculator
|
||||
|
||||
Try adding:
|
||||
|
||||
1. **Parentheses**: Parse `(2 + 3) * 4`
|
||||
2. **Variables**: Store results in named variables
|
||||
3. **Functions**: `sqrt`, `abs`, `pow`
|
||||
4. **History**: Use State effect to track previous results
|
||||
|
||||
### Adding Variables (Bonus)
|
||||
|
||||
```lux
|
||||
effect Variables {
|
||||
fn get(name: String): Option<Int>
|
||||
fn set(name: String, value: Int): Unit
|
||||
}
|
||||
|
||||
fn evalWithVars(e: Expr): Result<Int, String> with {Variables} =
|
||||
match e {
|
||||
Var(name) => {
|
||||
match Variables.get(name) {
|
||||
Some(v) => Ok(v),
|
||||
None => Err("Unknown variable: " + name)
|
||||
}
|
||||
},
|
||||
// ... other cases
|
||||
}
|
||||
|
||||
handler memoryVars: Variables {
|
||||
fn get(name) = resume(State.get(name))
|
||||
fn set(name, value) = {
|
||||
State.put(name, value)
|
||||
resume(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Code
|
||||
|
||||
See `examples/tutorials/calculator.lux` for the full implementation.
|
||||
|
||||
## What You Learned
|
||||
|
||||
- **ADTs** model structured data
|
||||
- **Pattern matching** destructures data cleanly
|
||||
- **Recursion** processes nested structures
|
||||
- **Result** handles errors without exceptions
|
||||
- **REPL loops** combine effects naturally
|
||||
|
||||
## Next Tutorial
|
||||
|
||||
[Todo App](todo.md) - Build a task manager with file persistence.
|
||||
380
docs/tutorials/dependency-injection.md
Normal file
380
docs/tutorials/dependency-injection.md
Normal file
@@ -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<String>
|
||||
}
|
||||
|
||||
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.
|
||||
325
docs/tutorials/project-ideas.md
Normal file
325
docs/tutorials/project-ideas.md
Normal file
@@ -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<String>, 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<Contact>
|
||||
fn list(): List<Contact>
|
||||
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<String>)
|
||||
| Link(String, String) // text, url
|
||||
|
||||
fn parseMarkdown(input: String): List<MarkdownNode> = ...
|
||||
fn toHtml(nodes: List<MarkdownNode>): 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<T>(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<Stmt>): Unit with {Runtime} = ...
|
||||
```
|
||||
|
||||
### 12. Task Scheduler
|
||||
Schedule and run tasks with dependencies.
|
||||
|
||||
**Skills**: Graphs, effects, async simulation
|
||||
|
||||
```lux
|
||||
type Task = { id: String, deps: List<String>, 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<String, String>): 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<T>
|
||||
fn await(future: Future<T>): T
|
||||
fn sleep(ms: Int): Unit
|
||||
}
|
||||
|
||||
fn parallel(): List<Int> 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!
|
||||
Reference in New Issue
Block a user