Functions and Closures
This chapter covers closures in Psy. For basic function documentation, see the Functions chapter.
Closures
Closures in Psy are anonymous functions that can capture variables from their surrounding scope. They use the |parameters| -> return_type { body } syntax.
Basic Closure Syntax
fn main() { // Simple closure that adds two numbers let add = |a: Felt, b: Felt| -> Felt { a + b }; let result = add(5, 3); assert_eq(result, 8, "5 + 3 should equal 8"); }
Closure Type Inference
Closures can often infer parameter and return types:
fn main() { // Explicit types let multiply = |x: Felt, y: Felt| -> Felt { x * y }; // Type inference (when context is clear) let numbers = [1, 2, 3, 4, 5]; let sum = 0; // The closure type is inferred from usage let add_to_sum = |n| { sum + n }; assert_eq(multiply(3, 4), 12, "3 * 4 should equal 12"); }
Variable Capture
Closures can capture variables from their surrounding scope:
fn main() { let multiplier = 10; let base_value = 5; // Closure captures multiplier and base_value from outer scope let calculate = |input: Felt| -> Felt { (base_value + input) * multiplier }; let result = calculate(3); assert_eq(result, 80, "(5 + 3) * 10 should equal 80"); }
Closures with Conditional Logic
fn main() { // Closure that finds maximum of two values let max = |a: Felt, b: Felt| -> Felt { if a > b { a } else { b } }; assert_eq(max(10, 7), 10, "max of 10 and 7 should be 10"); assert_eq(max(3, 9), 9, "max of 3 and 9 should be 9"); }
Using Closures with Arrays
fn main() { let numbers = [1, 2, 3, 4, 5]; let multiplier = 3; // Closure to transform array elements let transform = |x: Felt| -> Felt { x * multiplier }; // Manual application (Psy doesn't have built-in map) let mut transformed = [0; 5]; for i in 0u32..5u32 { transformed[i as usize] = transform(numbers[i as usize]); } assert_eq(transformed[0], 3, "1 * 3 should equal 3"); assert_eq(transformed[4], 15, "5 * 3 should equal 15"); }
Closures as Function Parameters
You can pass closures to functions:
// Function that takes a closure as parameter fn apply_operation(a: Felt, b: Felt, operation: fn(Felt, Felt) -> Felt) -> Felt { operation(a, b) } fn main() { let add = |x: Felt, y: Felt| -> Felt { x + y }; let multiply = |x: Felt, y: Felt| -> Felt { x * y }; let sum_result = apply_operation(5, 3, add); let product_result = apply_operation(5, 3, multiply); assert_eq(sum_result, 8, "5 + 3 should equal 8"); assert_eq(product_result, 15, "5 * 3 should equal 15"); }
Complex Closure Examples
Mathematical Operations
fn main() { let base = 2; // Closure for power calculation let power = |exponent: Felt| -> Felt { let mut result = 1; for i in 0u32..(exponent as u32) { result = result * base; } result }; assert_eq(power(3), 8, "2^3 should equal 8"); assert_eq(power(4), 16, "2^4 should equal 16"); }
Validation Logic
fn main() { let min_value = 10; let max_value = 100; // Closure for range validation let is_in_range = |value: Felt| -> bool { value >= min_value && value <= max_value }; assert(is_in_range(50), "50 should be in range"); assert(!is_in_range(5), "5 should not be in range"); assert(!is_in_range(150), "150 should not be in range"); }
Closure Limitations
Closures in Psy have some limitations compared to other languages:
- No Mutable Captures: Closures cannot mutate captured variables
- No Move Semantics: Variables are captured by value, not moved
- Simple Type System: Complex generic closures are not supported
fn main() { let mut counter = 0; // This would not work - cannot mutate captured variables: // let increment = || { counter = counter + 1; }; // Instead, return new values: let next_value = |current: Felt| -> Felt { current + 1 }; counter = next_value(counter); assert_eq(counter, 1, "Counter should be 1"); }
Practical Closure Use Cases
Configuration-based Operations
fn main() { let config_multiplier = 5; let config_offset = 10; // Closure encapsulates configuration let transform_value = |input: Felt| -> Felt { (input * config_multiplier) + config_offset }; let result1 = transform_value(3); // (3 * 5) + 10 = 25 let result2 = transform_value(7); // (7 * 5) + 10 = 45 assert_eq(result1, 25, "Transform of 3 should be 25"); assert_eq(result2, 45, "Transform of 7 should be 45"); }
Conditional Processing
fn main() { let threshold = 50; let bonus_rate = 2; // Closure for bonus calculation let calculate_bonus = |base_amount: Felt| -> Felt { if base_amount > threshold { base_amount * bonus_rate } else { base_amount } }; assert_eq(calculate_bonus(30), 30, "No bonus for 30"); assert_eq(calculate_bonus(60), 120, "Double bonus for 60"); }
Best Practices
- Keep closures simple - Complex logic should be in named functions
- Use descriptive variable names - Even in short closures
- Prefer explicit types - When the closure interface is important
- Consider function alternatives - For reusable logic
// Good: Simple, clear closure fn main() { let double = |x: Felt| -> Felt { x * 2 }; // Good: Explicit types for important interfaces let validator = |amount: Felt| -> bool { amount > 0 }; // Consider: Named function for complex logic fn complex_calculation(a: Felt, b: Felt, c: Felt) -> Felt { if a > b { a * c + b } else { b * c + a } } }
Summary
Closures in Psy provide a way to create anonymous functions that can capture variables from their environment. They are useful for:
- Simple transformations and calculations
- Configuration-based operations that capture settings
- Callback-style patterns when passing to other functions
- Encapsulating logic with captured context
While more limited than closures in some languages, Psy closures are sufficient for most functional programming patterns needed in smart contract development.