Higher-Order Functions
Higher-order functions are functions that take other functions as parameters or return functions as results.
Overview
Higher-order functions enable powerful abstraction and code reuse by treating functions as first-class values.
Functions as Parameters
Basic Example
import std:println
apply_twice = |fn, x| {
fn(fn(x))
}
double = |x| x * 2
increment = |x| x + 1
println(apply_twice(double, 3)) # 12 (double(double(3)))
println(apply_twice(increment, 5)) # 7 (increment(increment(5)))
Map
Transform each element in a collection:
import std:println
numbers = [1, 2, 3, 4, 5]
squared = numbers::map(|x| x * x)
println(squared) # [1, 4, 9, 16, 25]
Filter
Keep only elements that match a predicate:
import std:println
numbers = [1, 2, 3, 4, 5, 6]
evens = numbers::filter(|x| x % 2 == 0)
println(evens) # [2, 4, 6]
Fold
Combine elements into a single value with fold(initial, fn). This is the only
reducing method — there is no list::reduce():
import std:println
numbers = [1, 2, 3, 4, 5]
sum = numbers::fold(0, |acc, x| acc + x)
product = numbers::fold(1, |acc, x| acc * x)
println(sum) # 15
println(product) # 120
map, filter and fold are the built-in higher-order list methods. Anything else
(each, find, any, all, sort_by, group_by, …) you write yourself with a
loop through, as shown below.
Functions as Return Values
Function Factories
import std:println
make_multiplier = |factor| {
|x| x * factor
}
times_2 = make_multiplier(2)
times_10 = make_multiplier(10)
println(times_2(5)) # 10
println(times_10(5)) # 50
Configurable Functions
import std:println
create_validator = |min_length, pattern| {
check = |input| {
long_enough = input::length() >= min_length
matches = input ~ pattern
long_enough && matches
}
check
}
validate_username = create_validator(3, /^[a-zA-Z0-9_]+$/)
validate_password = create_validator(8, /^.*[A-Z].*[0-9].*$/)
println(validate_username("abc")) # true
println(validate_password("Pass1")) # false (too short)
Common Higher-Order Functions
ForEach
There is no list::each(); loop through is the way to run a function for every
element, and it wraps up neatly as a helper:
import std:println
for_each = |list, fn| {
loop through list with item {
fn(item)
}
}
names = ["Alice", "Bob", "Charlie"]
for_each(names, |name| println("Hello, ${name}!"))
Find
list::find() does not exist either. Write it with an early return:
import std:println
find = |list, predicate| {
loop through list with item {
predicate(item) && return item
}
nil
}
numbers = [1, 3, 5, 8, 10]
first_even = find(numbers, |x| x % 2 == 0)
println(first_even) # 8
Any / All
Same again — any and all are helpers you define, not methods:
import std:println
any = |list, predicate| {
loop through list with item {
predicate(item) && return true
}
false
}
all = |list, predicate| {
loop through list with item {
!predicate(item) && return false
}
true
}
numbers = [2, 4, 6, 8]
println(any(numbers, |x| x > 5)) # true
println(all(numbers, |x| x % 2 == 0)) # true
Sort By
There is no built-in list::sort_by(). If you need ordering by a key, write a small helper.
import std:println
insert_sorted = |list, item, key_fn| {
out = []
inserted = false
loop through list with existing {
match { !inserted && key_fn(item) < key_fn(existing) => {
out::push(item)
inserted = true
} }
out::push(existing)
}
match { inserted == false => { out::push(item) } }
out
}
sort_by = |list, key_fn| {
result = []
loop through list with item {
result = insert_sorted(result, item, key_fn)
}
result
}
users = [
{name: "Alice", age: 30},
{name: "Bob", age: 25},
{name: "Charlie", age: 35},
]
by_age = sort_by(users, |u| u:age)
println(by_age[0]:name) # Bob (youngest)
list::sort() sorts numbers and strings directly; the helper above is only needed when
you sort by a derived key.
Group By
Group elements by key function:
import std:println
group_by = |list, key_fn| {
result = {}
loop through list with item {
key = key_fn(item)
group = result::get(key, [])
group::push(item)
result[key] = group
}
result
}
users = [
{name: "Alice", role: "admin"},
{name: "Bob", role: "user"},
{name: "Charlie", role: "admin"},
]
by_role = group_by(users, |u| u:role)
println(by_role:admin::length()) # 2
Function Combinators
Compose
Combine two functions. Suji has built-in composition operators (>> and <<), but
writing compose by hand shows what they do:
import std:println
compose = |f, g| {
|x| f(g(x))
}
add_1 = |x| x + 1
times_2 = |x| x * 2
# (x + 1) * 2
add_then_multiply = compose(times_2, add_1)
println(add_then_multiply(5)) # 12
# The same thing with the operator
println((add_1 >> times_2)(5)) # 12
Pipe
Apply value through functions:
import std:println
pipe = |x, functions| {
result = x
loop through functions with fn {
result = fn(result)
}
result
}
result = pipe(5, [
|x| x + 1,
|x| x * 2,
|x| x ^ 2,
])
println(result) # 144
Partial
There are no variadic parameters, so a generic partial is not expressible. Bind the
known arguments in a closure with the arity you actually need:
import std:println
add_three = |a, b, c| a + b + c
partial_1 = |fn, first| {
rest = |b, c| fn(first, b, c)
rest
}
add_5_and = partial_1(add_three, 5)
println(add_5_and(3, 7)) # 15
Practical Examples
Data Pipeline
import std:println
process_data = |data, transformers| {
result = data
loop through transformers with transform {
result = transform(result)
}
result
}
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
result = process_data(data, [
|nums| nums::filter(|x| x % 2 == 0),
|nums| nums::map(|x| x * x),
|nums| nums::fold(0, |acc, x| acc + x)
])
println(result) # 220
Validation Pipeline
import std:println
validate_all = |value, validators| {
loop through validators with validator {
valid, error = validator(value)
!valid && return false, error
}
return true, nil
}
valid, error = validate_all("test@example.com", [
|v| match { v != nil => (true, nil), _ => (false, "Required"), },
|v| match { v::length() > 0 => (true, nil), _ => (false, "Not empty"), },
|v| match { v ~ /@/ => (true, nil), _ => (false, "Invalid email"), },
])
println(valid) # true
println(error) # nil
Retry Logic
A function that returns a (value, error) pair can be retried by a higher-order
wrapper. Suji has no exceptions, so the wrapped function reports failure in its return
value rather than raising:
import std:println
retry = |fn, max_attempts| {
attempt = 1
loop {
result, error = fn()
done = error == nil || attempt >= max_attempts
done && return result, error
attempt = attempt + 1
}
}
# A call that fails the first two times
attempts = 0
flaky = || {
attempts = attempts + 1
match {
attempts < 3 => (nil, "temporary failure"),
_ => ("payload", nil),
}
}
result, error = retry(flaky, 5)
println(result) # payload
println(attempts) # 3
Best Practices
DO:
- Use higher-order functions for abstraction
- Prefer the built-in methods (
map,filter,fold) over hand-written loops - Name function parameters descriptively
- Keep functions pure when possible
- Use lambdas for simple transformations
DON’T:
- Overuse higher-order functions
- Create deeply nested function calls
- Ignore performance implications
- Make functions too abstract
- Forget about readability