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:

  1. Evaluates the callee: Resolve the function value
  2. Evaluates arguments: Compute all argument values left-to-right
  3. Creates new environment: New scope with parent = function's closure
  4. Binds parameters: Maps parameter names to argument values
  5. Executes body: Runs function statements
  6. Handles return: Captures return value or returns nil
  7. 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:

  1. make_greeter("Hello") creates environment: { greeting: "Hello" }
  2. Inner function greet captures this environment as its closure
  3. When greet is called later, it can access greeting from its closure
  4. Each call to make_greeter creates a new closure with its own greeting

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

ConceptDescription
First-class functionsFunctions are values
Function declarationfn name(params) { body }
Lambda expressionfn(params) { body }
ClosureFunction + captured environment
Higher-order functionFunction taking/returning functions
RecursionFunction 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