Closures and Lambdas

AIScript treats functions as first-class citizens, enabling powerful functional programming patterns through closures and lambdas. These constructs allow you to write more concise, flexible, and expressive code.

Understanding Closures

A closure is a function that captures and remembers the environment in which it was created. This means it can access variables from its enclosing scope, even after that scope has completed execution.

fn create_counter(start: int) {
    let count = start;
    
    fn increment() -> int {
        count += 1;
        return count;
    }
    
    return increment;
}

let counter = create_counter(10);
print(counter()); // 11
print(counter()); // 12
print(counter()); // 13

In this example, the increment function forms a closure by capturing the count variable from its outer scope. Each time counter is called, it remembers and updates the same count variable.

Creating Lambdas

Lambdas (also called anonymous functions) are concise function expressions that you can define inline without naming. AIScript provides a simple syntax for lambdas using the pipe | character:

// Basic lambda syntax
let add = |a, b| { return a + b; };
print(add(5, 3)); // 8

// For single expressions, you can omit the braces and return statement
let multiply = |a, b| a * b;
print(multiply(4, 6)); // 24

Higher-Order Functions

Closures and lambdas shine when used with higher-order functions (functions that take other functions as arguments or return them):

// Map example
let numbers = [1, 2, 3, 4, 5];
let squared = map(numbers, |n| n * n);
print(squared); // [1, 4, 9, 16, 25]

// Filter example
let even_numbers = filter(numbers, |n| n % 2 == 0);
print(even_numbers); // [2, 4]

Capturing Variables

Closures can capture variables in three different ways:

1. By Reference (Default)

let x = 10;
let add_x = |n| n + x;
print(add_x(5)); // 15

x = 20;
print(add_x(5)); // 25 (reflects updated value of x)

2. Mutable Capture

When a closure modifies a captured variable, it affects the original:

let counter = 0;
let increment = || { counter += 1; return counter; };

print(increment()); // 1
print(increment()); // 2
print(counter);     // 2 (affected by the closure)

Immediate Invocation

You can immediately invoke a lambda after defining it:

let result = |x, y| {
    let sum = x + y;
    let product = x * y;
    return sum + product;
}(3, 4);

print(result); // 19 (sum: 7, product: 12)

Recursion with Closures

Closures can call themselves recursively:

// Using a named function for recursion
let factorial = |n| {
    fn fact(x) -> int {
        if x <= 1 {
            return 1;
        }
        return x * fact(x - 1);
    }
    
    return fact(n);
};

print(factorial(5)); // 120

Closures as Callbacks

Closures are ideal for event-driven or asynchronous programming:

fn fetch_data(url: str, on_success, on_error) {
    // Simulating an async operation
    if url.starts_with("https") {
        on_success(f"Data from {url}");
    } else {
        on_error(f"Invalid URL: {url}");
    }
}

fetch_data(
    "https://api.example.com",
    |data| print("Success:", data),
    |error| print("Error:", error)
);
Notice

AIScript current doesn't support fn as type annotation.

fn fetch_data(url: str, on_success: fn(data: str), on_error: fn(error: str)) {}

Performance Considerations

While closures provide powerful abstractions, they have memory implications since they maintain references to their captured environment. Consider these best practices:

  1. Limit capture scope: Only capture what you need
  2. Be mindful of memory: Large closures with many captures may impact performance
  3. Consider alternatives: For simple cases, regular functions might be more efficient

Closures and lambdas are fundamental to many modern programming patterns, from functional programming to asynchronous workflows. By mastering these concepts in AIScript, you'll be able to write more concise, maintainable, and powerful code.