Closures and first-class functions
Functions as values
In Part 1, you learned to define and call functions. But in Nyx, functions are more powerful than that — they are values, just like integers or strings. You can store a function in a variable, pass it to another function, or return it from a function.
This concept is called first-class functions, and it unlocks a whole new way of thinking about programs.
Passing functions to functions
Let's start with a simple example. Suppose you have two functions that transform a number:
fn double(n: int) -> int { return n * 2 } fn square(n: int) -> int { return n * n }
Now you want a function that applies any transformation to a number:
fn apply(f: Fn, x: int) -> int { return f(x) } fn main() { print(apply(double, 5)) // 10 print(apply(square, 5)) // 25 }
The type Fn means "a function." You pass double and square by name — no parentheses, because you are not calling them, you are passing them.
Why is this useful?
Imagine you want to apply a transformation to every element of an array. Without first-class functions, you would need to write a separate loop for each transformation. With them:
fn double(n: int) -> int { return n * 2 } fn negate(n: int) -> int { return 0 - n } fn add_ten(n: int) -> int { return n + 10 } fn map_array(arr: Array, f: Fn) -> Array { var result: Array = [] var i: int = 0 while i < arr.length() { result.push(f(arr[i])) i += 1 } return result } fn main() { let nums: Array = [1, 2, 3, 4, 5] let doubled: Array = map_array(nums, double) print(doubled) // [2, 4, 6, 8, 10] let negated: Array = map_array(nums, negate) print(negated) // [-1, -2, -3, -4, -5] let shifted: Array = map_array(nums, add_ten) print(shifted) // [11, 12, 13, 14, 15] }
One function (map_array) handles all three cases. The behavior changes based on which function you pass.
Filter and reduce
Two more patterns that work beautifully with first-class functions:
fn is_even(n: int) -> bool { return n % 2 == 0 } fn is_positive(n: int) -> bool { return n > 0 } fn filter_array(arr: Array, predicate: Fn) -> Array { var result: Array = [] var i: int = 0 while i < arr.length() { if predicate(arr[i]) { result.push(arr[i]) } i += 1 } return result } fn add(a: int, b: int) -> int { return a + b } fn multiply(a: int, b: int) -> int { return a * b } fn reduce_array(arr: Array, initial: int, f: Fn) -> int { var acc: int = initial var i: int = 0 while i < arr.length() { acc = f(acc, arr[i]) i += 1 } return acc } fn main() { let nums: Array = [1, 2, 3, 4, 5, 6, 7, 8] let evens: Array = filter_array(nums, is_even) print(evens) // [2, 4, 6, 8] let sum: int = reduce_array(nums, 0, add) print(sum) // 36 let product: int = reduce_array(nums, 1, multiply) print(product) // 40320 }
These three — map, filter, reduce — are the building blocks of data processing in many languages.
What is a closure?
A closure is a function that remembers variables from the scope where it was defined. In Nyx, you create closures by defining functions inside other functions:
fn make_adder(n: int) -> Fn { fn add(x: int) -> int { return n + x } return add } fn main() { let add5: Fn = make_adder(5) let add10: Fn = make_adder(10) print(add5(3)) // 8 print(add10(3)) // 13 print(add5(100)) // 105 }
When make_adder(5) runs, it creates a function add that remembers n = 5. Even after make_adder returns, the inner function still has access to n. That captured environment is what makes it a closure.
Closures capture variables
Closures can capture and even modify variables from their enclosing scope:
fn counter() -> int { var count: int = 0 fn increment() { count = count + 1 } fn get_count() -> int { return count } increment() increment() increment() return get_count() } fn main() { print(counter()) // 3 }
Both increment and get_count share the same count variable. When increment modifies it, get_count sees the updated value.
Closures as accumulators
fn accumulate(n: int) -> int { var total: int = 0 fn add_to_total(x: int) { total = total + x } var i: int = 1 while i <= n { add_to_total(i) i = i + 1 } return total } fn main() { print(accumulate(10)) // 55 (1+2+3+...+10) print(accumulate(5)) // 15 (1+2+3+4+5) }
Applying functions multiple times
fn apply_twice(f: Fn, x: int) -> int { return f(f(x)) } fn apply_n_times(f: Fn, x: int, n: int) -> int { var result: int = x var i: int = 0 while i < n { result = f(result) i += 1 } return result } fn double(n: int) -> int { return n * 2 } fn increment(n: int) -> int { return n + 1 } fn main() { print(apply_twice(double, 3)) // 12 (3*2=6, 6*2=12) print(apply_n_times(double, 1, 10)) // 1024 (2^10) print(apply_n_times(increment, 0, 100)) // 100 }
Practical example: sorting with a comparator
You can use first-class functions to make a generic sort:
fn sort_array(arr: Array, compare: Fn) -> Array { // Simple bubble sort var result: Array = [] var i: int = 0 while i < arr.length() { result.push(arr[i]) i += 1 } var swapped: bool = true while swapped { swapped = false i = 0 while i < result.length() - 1 { if compare(result[i], result[i + 1]) > 0 { let temp: int = result[i] result[i] = result[i + 1] result[i + 1] = temp swapped = true } i += 1 } } return result } fn ascending(a: int, b: int) -> int { return a - b } fn descending(a: int, b: int) -> int { return b - a } fn main() { let nums: Array = [5, 2, 8, 1, 9, 3] let asc: Array = sort_array(nums, ascending) print(asc) // [1, 2, 3, 5, 8, 9] let desc: Array = sort_array(nums, descending) print(desc) // [9, 8, 5, 3, 2, 1] }
Same sort algorithm, different behavior based on the function you pass.
Practical example: event callbacks
Closures are perfect for event-driven patterns:
fn on_threshold(values: Array, threshold: int, callback: Fn) { var i: int = 0 while i < values.length() { if values[i] > threshold { callback(values[i]) } i += 1 } } fn alert(value: int) { print("WARNING: value " + int_to_string(value) + " exceeds threshold!") } fn main() { let temperatures: Array = [22, 35, 28, 41, 19, 38, 45] on_threshold(temperatures, 37, alert) }
Output:
WARNING: value 41 exceeds threshold! WARNING: value 38 exceeds threshold! WARNING: value 45 exceeds threshold!
Exercises
- Write a function
apply_to_all(arr: Array, f: Fn) -> Arraythat applies a function to every element and returns a new array. Test it with at least three different functions.
- Write
count_matching(arr: Array, predicate: Fn) -> intthat counts how many elements satisfy a predicate function.
- Write
compose(f: Fn, g: Fn, x: int) -> intthat appliesgfirst, thenf:compose(f, g, x)equalsf(g(x)).
- Create a
make_multiplier(factor: int) -> Fnclosure that returns a function which multiplies its argument byfactor. Use it to createtripleandquadruplefunctions.
- Write a
find_first(arr: Array, predicate: Fn) -> intthat returns the first element matching the predicate, or-1if none match.
Summary
- Functions are values in Nyx — you can pass them, store them, and return them.
- The
Fntype represents a function value. - Pass a function by name (without parentheses):
apply(double, 5). - Map, filter, and reduce are powerful patterns built on first-class functions.
- A closure is a function that captures variables from its enclosing scope.
- Closures can read and modify captured variables.
- Use closures to create functions with built-in state (counters, adders, multipliers).
Next chapter: Enums and pattern matching →