Functions & Closures
Functions are the heart of Lux. In this chapter, we'll dive deep into how functions work as first-class values and how closures capture their environment.
First-Class Functions
In Lux, functions are values just like numbers or strings:
fn add(a, b) {
return a + b;
}
let operation = add; // Assign function to variable
print operation(5, 3); // Call through variable: 8
Function Declaration vs Lambda
Lux supports two ways to create functions:
1. Function Declaration (Statement)
fn greet(name) {
return "Hello, " + name;
}
This creates a named function in the current scope.
2. Lambda Expression (Anonymous Function)
let greet = fn(name) {
return "Hello, " + name;
};
This creates an anonymous function as an expression.
Both create the same type of value: Value::Function.
How Function Calls Work
When you call a function, the interpreter:
- Evaluates the callee: Resolve the function value
- Evaluates arguments: Compute all argument values left-to-right
- Creates new environment: New scope with parent = function's closure
- Binds parameters: Maps parameter names to argument values
- Executes body: Runs function statements
- Handles return: Captures return value or returns nil
- Restores environment: Pops back to caller's scope
fn add(a, b) {
return a + b;
}
let result = add(2, 3);
Execution trace:
1. Evaluate callee: add → Value::Function { params: ["a", "b"], body: [...], closure: global_env }
2. Evaluate args: 2 → 2, 3 → 3
3. Create new environment: { parent: global_env }
4. Bind parameters: { a: 2, b: 3 }
5. Execute body: return a + b
- Evaluate a + b: 2 + 3 = 5
- Return 5
6. Result: 5
7. Restore caller environment
Recursion
Functions can call themselves:
fn factorial(n) {
if n <= 1 {
return 1;
}
return n * factorial(n - 1);
}
print factorial(5); // 120
Execution for factorial(3):
factorial(3)
→ 3 <= 1? No
→ 3 * factorial(2)
→ 2 <= 1? No
→ 2 * factorial(1)
→ 1 <= 1? Yes
→ return 1
→ 2 * 1 = 2
→ 3 * 2 = 6
The call stack grows with each recursive call. Too many recursions cause stack overflow.
Closures: Capturing the Environment
A closure is a function that "closes over" (captures) variables from its surrounding scope.
Simple Closure Example
fn make_greeter(greeting) {
fn greet(name) {
return greeting + ", " + name + "!";
}
return greet;
}
let hello_greeter = make_greeter("Hello");
let hi_greeter = make_greeter("Hi");
print hello_greeter("Alice"); // "Hello, Alice!"
print hi_greeter("Bob"); // "Hi, Bob!"
How it works:
make_greeter("Hello")creates environment:{ greeting: "Hello" }- Inner function
greetcaptures this environment as its closure - When
greetis called later, it can accessgreetingfrom its closure - Each call to
make_greetercreates a new closure with its owngreeting
Counter Example
fn make_counter() {
let count = 0;
fn increment() {
count = count + 1;
return count;
}
return increment;
}
let counter1 = make_counter();
print counter1(); // 1
print counter1(); // 2
print counter1(); // 3
let counter2 = make_counter();
print counter2(); // 1 (separate counter)
Each counter has its own count variable stored in its closure.
Implementation: Closure Capture
Our implementation (from chapter 4) already supports closures! Let's review how:
When Function is Defined
Stmt::Function { name, params, body } => {
let func = Value::Function {
params,
body,
closure: Rc::clone(&self.environment), // Capture current environment!
};
self.environment.borrow_mut().define(name, func);
Ok(None)
}
The function captures self.environment - the environment where it was defined, not where it's called.
When Function is Called
Value::Function { params, body, closure } => {
// Create new environment with closure as parent, NOT current environment!
let new_env = Environment::with_parent(closure);
// ... bind parameters and execute ...
}
The new function environment's parent is the closure, so the function can access variables from where it was defined.
Higher-Order Functions
Functions that take other functions as arguments or return functions.
Map Function
fn map(list, f) {
let result = [];
for item in list {
push(result, f(item));
}
return result;
}
fn double(x) {
return x * 2;
}
let numbers = [1, 2, 3, 4, 5];
let doubled = map(numbers, double);
print doubled; // [2, 4, 6, 8, 10]
Anonymous Function as Argument
let squared = map([1, 2, 3, 4], fn(x) {
return x * x;
});
print squared; // [1, 4, 9, 16]
Filter Function
fn filter(list, predicate) {
let result = [];
for item in list {
if predicate(item) {
push(result, item);
}
}
return result;
}
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let evens = filter(numbers, fn(x) {
return x % 2 == 0;
});
print evens; // [2, 4, 6, 8, 10]
Reduce/Fold Function
fn reduce(list, initial, f) {
let result = initial;
for item in list {
result = f(result, item);
}
return result;
}
let sum = reduce([1, 2, 3, 4, 5], 0, fn(acc, x) {
return acc + x;
});
print sum; // 15
let product = reduce([1, 2, 3, 4, 5], 1, fn(acc, x) {
return acc * x;
});
print product; // 120
Closure Patterns
1. Private State
fn make_bank_account(initial_balance) {
let balance = initial_balance;
fn deposit(amount) {
balance = balance + amount;
return balance;
}
fn withdraw(amount) {
if amount > balance {
print "Insufficient funds";
return balance;
}
balance = balance - amount;
return balance;
}
fn get_balance() {
return balance;
}
// Return object-like structure (in real implementation, would be a map)
return fn(method, amount) {
if method == "deposit" {
return deposit(amount);
} else if method == "withdraw" {
return withdraw(amount);
} else if method == "balance" {
return get_balance();
}
};
}
let account = make_bank_account(100);
print account("balance", 0); // 100
print account("deposit", 50); // 150
print account("withdraw", 30); // 120
2. Partial Application
fn make_adder(x) {
return fn(y) {
return x + y;
};
}
let add5 = make_adder(5);
let add10 = make_adder(10);
print add5(3); // 8
print add10(3); // 13
3. Callback Pattern
fn repeat(n, action) {
let i = 0;
while i < n {
action(i);
i = i + 1;
}
}
repeat(3, fn(i) {
print "Iteration " + str(i);
});
// Output:
// Iteration 0
// Iteration 1
// Iteration 2
4. Memoization (Simple)
fn make_memoized_fibonacci() {
let cache = [0, 1]; // Base cases
fn fib(n) {
if n < len(cache) {
return cache[n];
}
let result = fib(n - 1) + fib(n - 2);
push(cache, result);
return result;
}
return fib;
}
let fast_fib = make_memoized_fibonacci();
print fast_fib(10); // Much faster than naive recursion
Recursion with Closures
Closures work with recursion:
fn make_factorial() {
fn factorial(n) {
if n <= 1 {
return 1;
}
return n * factorial(n - 1);
}
return factorial;
}
let fact = make_factorial();
print fact(5); // 120
The inner factorial function captures the environment where factorial itself is defined, allowing it to call itself recursively.
Testing Functions and Closures
Create a test file examples/functions_test.lux:
// Test 1: Basic function
fn add(a, b) {
return a + b;
}
print "Test 1: " + str(add(2, 3) == 5);
// Test 2: Higher-order function
fn apply(f, x) {
return f(x);
}
print "Test 2: " + str(apply(fn(n) { return n * 2; }, 5) == 10);
// Test 3: Closure
fn make_counter() {
let count = 0;
return fn() {
count = count + 1;
return count;
};
}
let c = make_counter();
print "Test 3a: " + str(c() == 1);
print "Test 3b: " + str(c() == 2);
print "Test 3c: " + str(c() == 3);
// Test 4: Multiple closures
let c1 = make_counter();
let c2 = make_counter();
print "Test 4a: " + str(c1() == 1);
print "Test 4b: " + str(c2() == 1);
print "Test 4c: " + str(c1() == 2);
// Test 5: Nested closures
fn outer(x) {
fn middle(y) {
fn inner(z) {
return x + y + z;
}
return inner;
}
return middle;
}
let f = outer(1)(2);
print "Test 5: " + str(f(3) == 6);
// Test 6: Recursion
fn fibonacci(n) {
if n <= 1 {
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
print "Test 6: " + str(fibonacci(10) == 55);
print "All tests completed!";
Run it:
cargo run -- examples/functions_test.lux
Common Pitfalls
1. Variable Capture Confusion
// ❌ Common mistake: expecting each function to capture different i
let functions = [];
let i = 0;
while i < 3 {
push(functions, fn() { return i; });
i = i + 1;
}
// All functions return 3! They all capture the same i variable.
print functions[0](); // 3
print functions[1](); // 3
print functions[2](); // 3
// ✅ Correct: create new scope for each iteration
fn make_functions() {
let functions = [];
let i = 0;
while i < 3 {
fn make_function(n) {
return fn() { return n; };
}
push(functions, make_function(i));
i = i + 1;
}
return functions;
}
let funcs = make_functions();
print funcs[0](); // 0
print funcs[1](); // 1
print funcs[2](); // 2
2. Stack Overflow from Deep Recursion
// ❌ Will crash with stack overflow
fn count_to_million(n) {
if n >= 1000000 {
return n;
}
return count_to_million(n + 1);
}
print count_to_million(0);
// ✅ Use iteration instead
fn count_to_million(target) {
let n = 0;
while n < target {
n = n + 1;
}
return n;
}
print count_to_million(1000000);
3. Forgetting Return Statement
// ❌ Returns nil, not the computed value
fn add(a, b) {
a + b; // Expression statement, result discarded
}
print add(2, 3); // nil
// ✅ Explicit return
fn add(a, b) {
return a + b;
}
print add(2, 3); // 5
Performance Considerations
Closure Memory
Each closure stores a reference to its environment:
fn make_counters(n) {
let counters = [];
let i = 0;
while i < n {
let count = 0; // New variable for each counter
push(counters, fn() {
count = count + 1;
return count;
});
i = i + 1;
}
return counters;
}
// Creates n closures, each with its own environment
let counters = make_counters(1000);
Each counter holds a separate environment - this uses memory.
Recursion Depth
Lux uses the Rust call stack for recursion. Deep recursion exhausts the stack:
// Safe: ~1000 calls
print fibonacci(20);
// Dangerous: ~2^30 calls (exponential!)
print fibonacci(30); // Takes forever
// Stack overflow: too deep
print fibonacci(100); // Crash
Use iteration for deep or expensive operations.
Summary
| Concept | Description |
|---|---|
| First-class functions | Functions are values |
| Function declaration | fn name(params) { body } |
| Lambda expression | fn(params) { body } |
| Closure | Function + captured environment |
| Higher-order function | Function taking/returning functions |
| Recursion | Function calling itself |
What's Next?
We have a powerful function system! Now let's add more language features: lists, advanced loops, and string operations.
→ Continue to Chapter 6: Advanced Features