feat: rebuild website with full learning funnel
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>
This commit is contained in:
419
examples/showcase/task_manager.lux
Normal file
419
examples/showcase/task_manager.lux
Normal file
@@ -0,0 +1,419 @@
|
||||
// =============================================================================
|
||||
// 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 {}
|
||||
Reference in New Issue
Block a user