Website rebuilt from scratch based on analysis of 11 beloved language websites (Elm, Zig, Gleam, Swift, Kotlin, Haskell, OCaml, Crystal, Roc, Rust, Go). New website structure: - Homepage with hero, playground, three pillars, install guide - Language Tour with interactive lessons (hello world, types, effects) - Examples cookbook with categorized sidebar - API documentation index - Installation guide (Nix and source) - Sleek/noble design (black/gold, serif typography) Also includes: - New stdlib/json.lux module for JSON serialization - Enhanced stdlib/http.lux with middleware and routing - New string functions (charAt, indexOf, lastIndexOf, repeat) - LSP improvements (rename, signature help, formatting) - Package manager transitive dependency resolution - Updated documentation for effects and stdlib - New showcase example (task_manager.lux) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
420 lines
14 KiB
Plaintext
420 lines
14 KiB
Plaintext
// =============================================================================
|
|
// Task Manager API - A Showcase of Lux's Unique Features
|
|
// =============================================================================
|
|
//
|
|
// This example demonstrates Lux's three killer features:
|
|
//
|
|
// 1. ALGEBRAIC EFFECTS - Every side effect is explicit in function signatures
|
|
// - No hidden I/O, no surprise database calls
|
|
// - Testing is trivial: just swap handlers
|
|
//
|
|
// 2. BEHAVIORAL TYPES - Compile-time guarantees about function behavior
|
|
// - `is pure` - no side effects, safe to cache
|
|
// - `is total` - always terminates, never fails
|
|
// - `is idempotent` - safe to retry without side effects
|
|
// - `is deterministic` - same input = same output
|
|
//
|
|
// 3. SCHEMA EVOLUTION - Versioned types with automatic migration
|
|
// - Data structures evolve safely over time
|
|
// - Old data automatically upgrades
|
|
//
|
|
// To run: lux run examples/showcase/task_manager.lux
|
|
// =============================================================================
|
|
|
|
|
|
// =============================================================================
|
|
// PART 1: VERSIONED DATA TYPES (Schema Evolution)
|
|
// =============================================================================
|
|
|
|
// Task v1: Our original data model (simple)
|
|
type Task @v1 {
|
|
id: String,
|
|
title: String,
|
|
done: Bool
|
|
}
|
|
|
|
// Task v2: Added priority field
|
|
// The `from @v1` clause defines how to migrate old data automatically
|
|
type Task @v2 {
|
|
id: String,
|
|
title: String,
|
|
done: Bool,
|
|
priority: String, // New field: "low", "medium", "high"
|
|
|
|
// Migration: old tasks get "medium" priority by default
|
|
from @v1 = {
|
|
id: old.id,
|
|
title: old.title,
|
|
done: old.done,
|
|
priority: "medium"
|
|
}
|
|
}
|
|
|
|
// Task v3: Added due date and tags
|
|
// Migrations chain automatically: v1 → v2 → v3
|
|
type Task @v3 {
|
|
id: String,
|
|
title: String,
|
|
done: Bool,
|
|
priority: String,
|
|
dueDate: Option<Int>, // Unix timestamp, optional
|
|
tags: List<String>, // New: categorization
|
|
|
|
from @v2 = {
|
|
id: old.id,
|
|
title: old.title,
|
|
done: old.done,
|
|
priority: old.priority,
|
|
dueDate: None, // No due date for migrated tasks
|
|
tags: [] // Empty tags for migrated tasks
|
|
}
|
|
}
|
|
|
|
// Use @latest to always refer to the newest version
|
|
type TaskList = List<Task@latest>
|
|
|
|
|
|
// =============================================================================
|
|
// PART 2: PURE FUNCTIONS WITH BEHAVIORAL TYPES
|
|
// =============================================================================
|
|
|
|
// Pure function: no side effects, safe to cache, parallelize, eliminate if unused
|
|
// The compiler verifies `is pure` - if you try to call an effect, it errors.
|
|
fn formatTask(task: Task@latest): String
|
|
is pure
|
|
is deterministic
|
|
is total = {
|
|
let status = if task.done then "[x]" else "[ ]"
|
|
let priority = match task.priority {
|
|
"high" => "!!",
|
|
"medium" => "!",
|
|
_ => ""
|
|
}
|
|
status + " " + priority + task.title
|
|
}
|
|
|
|
// Idempotent function: f(f(x)) = f(x)
|
|
// Safe to apply multiple times without changing the result
|
|
// Critical for retry logic - the compiler verifies this property
|
|
fn normalizeTitle(title: String): String
|
|
is pure
|
|
is idempotent = {
|
|
title
|
|
|> String.trim
|
|
|> String.toLower
|
|
}
|
|
|
|
// Total function: always terminates, never throws
|
|
// No Fail effect allowed, recursion must be structurally decreasing
|
|
fn countCompleted(tasks: TaskList): Int
|
|
is pure
|
|
is total = {
|
|
match tasks {
|
|
[] => 0,
|
|
[task, ...rest] =>
|
|
(if task.done then 1 else 0) + countCompleted(rest)
|
|
}
|
|
}
|
|
|
|
// Commutative function: f(a, b) = f(b, a)
|
|
// Enables parallel reduction and argument reordering optimizations
|
|
fn maxPriority(a: String, b: String): String
|
|
is pure
|
|
is commutative = {
|
|
let priorityValue = fn(p: String): Int =>
|
|
match p {
|
|
"high" => 3,
|
|
"medium" => 2,
|
|
"low" => 1,
|
|
_ => 0
|
|
}
|
|
if priorityValue(a) > priorityValue(b) then a else b
|
|
}
|
|
|
|
// Filter tasks by criteria - pure, can be cached and parallelized
|
|
fn filterByPriority(tasks: TaskList, priority: String): TaskList
|
|
is pure
|
|
is deterministic = {
|
|
List.filter(tasks, fn(t: Task@latest): Bool => t.priority == priority)
|
|
}
|
|
|
|
fn filterPending(tasks: TaskList): TaskList
|
|
is pure
|
|
is deterministic = {
|
|
List.filter(tasks, fn(t: Task@latest): Bool => !t.done)
|
|
}
|
|
|
|
fn filterCompleted(tasks: TaskList): TaskList
|
|
is pure
|
|
is deterministic = {
|
|
List.filter(tasks, fn(t: Task@latest): Bool => t.done)
|
|
}
|
|
|
|
|
|
// =============================================================================
|
|
// PART 3: EFFECTS - EXPLICIT SIDE EFFECTS
|
|
// =============================================================================
|
|
|
|
// Custom effect for task storage
|
|
// This declares WHAT operations are available, not HOW they work
|
|
effect TaskStore {
|
|
fn save(task: Task@latest): Result<Task@latest, String>
|
|
fn getById(id: String): Option<Task@latest>
|
|
fn getAll(): TaskList
|
|
fn delete(id: String): Bool
|
|
}
|
|
|
|
// Service functions declare their effects in the type signature
|
|
// Anyone reading the signature knows exactly what side effects can occur
|
|
|
|
// Create a new task - requires TaskStore and Random effects
|
|
fn createTask(title: String, priority: String): Task@latest
|
|
with {TaskStore, Random} = {
|
|
let id = "task_" + toString(Random.int(10000, 99999))
|
|
let task = {
|
|
id: id,
|
|
title: normalizeTitle(title), // Uses our idempotent normalizer
|
|
done: false,
|
|
priority: priority,
|
|
dueDate: None,
|
|
tags: []
|
|
}
|
|
match TaskStore.save(task) {
|
|
Ok(saved) => saved,
|
|
Err(_) => task // Return unsaved if storage fails
|
|
}
|
|
}
|
|
|
|
// Complete a task - idempotent, safe to retry
|
|
// If the network fails mid-request, retry is safe
|
|
fn completeTask(id: String): Option<Task@latest>
|
|
is idempotent // Compiler verifies this is safe to retry
|
|
with {TaskStore} = {
|
|
match TaskStore.getById(id) {
|
|
None => None,
|
|
Some(task) => {
|
|
// Setting done = true is idempotent: already done? stays done
|
|
let updated = { ...task, done: true }
|
|
match TaskStore.save(updated) {
|
|
Ok(saved) => Some(saved),
|
|
Err(_) => None
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get task summary - logging effect, but computation is pure
|
|
fn getTaskSummary(): { total: Int, completed: Int, pending: Int, highPriority: Int }
|
|
with {TaskStore, Logger} = {
|
|
let tasks = TaskStore.getAll()
|
|
Logger.log("Fetched " + toString(List.length(tasks)) + " tasks")
|
|
|
|
// These computations are pure - could be parallelized
|
|
let completed = countCompleted(tasks)
|
|
let pending = List.length(tasks) - completed
|
|
let highPriority = List.length(filterByPriority(tasks, "high"))
|
|
|
|
{ total: List.length(tasks), completed: completed, pending: pending, highPriority: highPriority }
|
|
}
|
|
|
|
|
|
// =============================================================================
|
|
// PART 4: EFFECT HANDLERS - SWAP IMPLEMENTATIONS
|
|
// =============================================================================
|
|
|
|
// In-memory handler for testing
|
|
// This handler stores tasks in a mutable list - perfect for unit tests
|
|
handler InMemoryTaskStore: TaskStore {
|
|
let tasks: List<Task@latest> = []
|
|
|
|
fn save(task: Task@latest): Result<Task@latest, String> = {
|
|
// Remove existing task with same ID (if any), then add new
|
|
tasks = List.filter(tasks, fn(t: Task@latest): Bool => t.id != task.id)
|
|
tasks = List.concat(tasks, [task])
|
|
Ok(task)
|
|
}
|
|
|
|
fn getById(id: String): Option<Task@latest> = {
|
|
List.find(tasks, fn(t: Task@latest): Bool => t.id == id)
|
|
}
|
|
|
|
fn getAll(): TaskList = tasks
|
|
|
|
fn delete(id: String): Bool = {
|
|
let before = List.length(tasks)
|
|
tasks = List.filter(tasks, fn(t: Task@latest): Bool => t.id != task.id)
|
|
List.length(tasks) < before
|
|
}
|
|
}
|
|
|
|
// Logging handler - wraps another handler with logging
|
|
handler LoggingTaskStore(inner: TaskStore): TaskStore with {Logger} {
|
|
fn save(task: Task@latest): Result<Task@latest, String> = {
|
|
Logger.log("Saving task: " + task.id)
|
|
inner.save(task)
|
|
}
|
|
|
|
fn getById(id: String): Option<Task@latest> = {
|
|
Logger.log("Getting task: " + id)
|
|
inner.getById(id)
|
|
}
|
|
|
|
fn getAll(): TaskList = {
|
|
Logger.log("Getting all tasks")
|
|
inner.getAll()
|
|
}
|
|
|
|
fn delete(id: String): Bool = {
|
|
Logger.log("Deleting task: " + id)
|
|
inner.delete(id)
|
|
}
|
|
}
|
|
|
|
// Simple logger effect and handler
|
|
effect Logger {
|
|
fn log(message: String): Unit
|
|
}
|
|
|
|
handler ConsoleLogger: Logger with {Console} {
|
|
fn log(message: String): Unit = {
|
|
Console.print("[LOG] " + message)
|
|
}
|
|
}
|
|
|
|
handler SilentLogger: Logger {
|
|
fn log(message: String): Unit = {
|
|
// Do nothing - useful for tests
|
|
}
|
|
}
|
|
|
|
|
|
// =============================================================================
|
|
// PART 5: TESTING - SWAP HANDLERS, NO MOCKS NEEDED
|
|
// =============================================================================
|
|
|
|
// Test helper: creates a controlled environment
|
|
fn runTestScenario(): Unit with {Console} = {
|
|
Console.print("=== Running Test Scenario ===")
|
|
Console.print("")
|
|
|
|
// Use in-memory storage and silent logging for tests
|
|
// No database, no file I/O, no network - pure in-memory testing
|
|
let result = run {
|
|
// Create some tasks
|
|
let task1 = createTask("Write documentation", "high")
|
|
let task2 = createTask("Fix bug #123", "medium")
|
|
let task3 = createTask("Review PR", "low")
|
|
|
|
// Complete one task
|
|
completeTask(task1.id)
|
|
|
|
// Get summary
|
|
getTaskSummary()
|
|
} with {
|
|
TaskStore = InMemoryTaskStore,
|
|
Logger = SilentLogger,
|
|
Random = {
|
|
// Deterministic "random" for tests
|
|
let counter = 0
|
|
fn int(min: Int, max: Int): Int = {
|
|
counter = counter + 1
|
|
min + (counter * 12345) % (max - min)
|
|
}
|
|
}
|
|
}
|
|
|
|
Console.print("Test Results:")
|
|
Console.print(" Total tasks: " + toString(result.total))
|
|
Console.print(" Completed: " + toString(result.completed))
|
|
Console.print(" Pending: " + toString(result.pending))
|
|
Console.print(" High priority: " + toString(result.highPriority))
|
|
Console.print("")
|
|
|
|
// Verify results
|
|
if result.total == 3 &&
|
|
result.completed == 1 &&
|
|
result.pending == 2 &&
|
|
result.highPriority == 1 {
|
|
Console.print("All tests passed!")
|
|
} else {
|
|
Console.print("Test failed!")
|
|
}
|
|
}
|
|
|
|
|
|
// =============================================================================
|
|
// PART 6: SCHEMA MIGRATION DEMO
|
|
// =============================================================================
|
|
|
|
fn demonstrateMigration(): Unit with {Console} = {
|
|
Console.print("=== Schema Evolution Demo ===")
|
|
Console.print("")
|
|
|
|
// Simulate loading a v1 task (from old database/API)
|
|
let oldTask = Schema.versioned("Task", 1, {
|
|
id: "legacy_001",
|
|
title: "Old task from v1",
|
|
done: false
|
|
})
|
|
|
|
Console.print("Loaded v1 task:")
|
|
Console.print(" Version: " + toString(Schema.getVersion(oldTask)))
|
|
Console.print("")
|
|
|
|
// Migrate to latest version automatically
|
|
let migratedTask = Schema.migrate(oldTask, 3)
|
|
|
|
Console.print("After migration to v3:")
|
|
Console.print(" Version: " + toString(Schema.getVersion(migratedTask)))
|
|
Console.print(" Has priority: " + migratedTask.priority) // Added by v2 migration
|
|
Console.print(" Has tags: " + toString(List.length(migratedTask.tags)) + " tags") // Added by v3
|
|
Console.print("")
|
|
Console.print("Old data seamlessly upgraded!")
|
|
}
|
|
|
|
|
|
// =============================================================================
|
|
// PART 7: MAIN - PUTTING IT ALL TOGETHER
|
|
// =============================================================================
|
|
|
|
fn main(): Unit with {Console} = {
|
|
Console.print("╔═══════════════════════════════════════════════════════════╗")
|
|
Console.print("║ Lux Task Manager - Feature Showcase ║")
|
|
Console.print("╚═══════════════════════════════════════════════════════════╝")
|
|
Console.print("")
|
|
|
|
// Demonstrate pure functions
|
|
Console.print("--- Pure Functions (Behavioral Types) ---")
|
|
let sampleTask = {
|
|
id: "demo",
|
|
title: "Learn Lux",
|
|
done: false,
|
|
priority: "high",
|
|
dueDate: None,
|
|
tags: ["learning", "programming"]
|
|
}
|
|
Console.print("Formatted task: " + formatTask(sampleTask))
|
|
Console.print("Normalized title: " + normalizeTitle(" HELLO WORLD "))
|
|
Console.print("")
|
|
|
|
// Demonstrate schema evolution
|
|
demonstrateMigration()
|
|
Console.print("")
|
|
|
|
// Run tests with swapped handlers
|
|
runTestScenario()
|
|
Console.print("")
|
|
|
|
Console.print("╔═══════════════════════════════════════════════════════════╗")
|
|
Console.print("║ Key Takeaways: ║")
|
|
Console.print("║ ║")
|
|
Console.print("║ 1. Effects in signatures = no hidden side effects ║")
|
|
Console.print("║ 2. Behavioral types = compile-time guarantees ║")
|
|
Console.print("║ 3. Handler swapping = easy testing without mocks ║")
|
|
Console.print("║ 4. Schema evolution = safe data migrations ║")
|
|
Console.print("╚═══════════════════════════════════════════════════════════╝")
|
|
}
|
|
|
|
// Run the showcase
|
|
let _ = run main() with {}
|