Higher-order functions
Higher-order functions (HOF) are functions that take other functions as arguments. The hof module provides combinators—functions that combine or apply other functions in useful patterns.
Why combinators?
In stack-based programming, you often need to:
- Apply multiple operations to the same value
- Keep a value while also processing it
- Apply operations conditionally
Without combinators, you need temporary variables:
fn main() {
// Without combinators: Is n both positive AND even?
6 -> n
n 0 > -> is_positive
n 2 % 0 == -> is_even
is_positive is_even and print nl // 1
}
With combinators, it's one line:
use hof
fn main() {
6 fn (x:i64 -- r:i64) { 0 > } fn (x:i64 -- r:i64) { 2 % 0 == } hof::bi and print nl // 1
}
Available combinators
apply
Apply a single function to a value.
use hof
fn main() {
5 fn (x:i64 -- r:i64) { 2 * } hof::apply print nl // 10
}
bi
Apply two functions to the same value.
use hof
fn main() {
5 fn (x:i64 -- r:i64) { 2 * } fn (x:i64 -- r:i64) { 3 + } hof::bi
print nl print nl // 8 then 10 (5+3=8, 5*2=10)
}
tri
Apply three functions to the same value.
use hof
fn main() {
5 fn (x:i64 -- r:i64) { 1 + } fn (x:i64 -- r:i64) { 2 * } fn (x:i64 -- r:i64) { dup * } hof::tri
print nl print nl print nl // 25 then 10 then 6 (5*5=25, 5*2=10, 5+1=6)
}
keep
Apply a function but preserve the original value.
use hof
fn main() {
5 fn (x:i64 -- r:i64) { 2 * } hof::keep
print nl print nl // 5 then 10 (original=5 preserved, result=10)
}
dip
Apply a function to the second element, preserving the top.
use hof
fn main() {
10 20 fn (x:i64 -- r:i64) { 2 * } hof::dip
print nl print nl // 20 then 20 (kept 20 on top, doubled 10 to 20)
}
both
Apply the same function to two values.
use hof
fn main() {
3 4 fn (x:i64 -- r:i64) { dup * } hof::both
print nl print nl // 16 then 9 (4*4=16, 3*3=9)
}
bi_star
Apply different functions to two values.
use hof
fn main() {
3 4 fn (x:i64 -- r:i64) { 1 + } fn (x:i64 -- r:i64) { 2 * } hof::bi_star
print nl print nl // 8 then 4 (4*2=8, 3+1=4)
}
when
Apply function only if condition is true.
use hof
fn main() {
5 1 fn (x:i64 -- r:i64) { 2 * } hof::when print nl // 10 (condition true)
5 0 fn (x:i64 -- r:i64) { 2 * } hof::when print nl // 5 (condition false, unchanged)
}
unless
Apply function only if condition is false (opposite of when).
use hof
fn main() {
5 0 fn (x:i64 -- r:i64) { 2 * } hof::unless print nl // 10 (condition false)
5 1 fn (x:i64 -- r:i64) { 2 * } hof::unless print nl // 5 (condition true, unchanged)
}
times
Apply a function n times to an initial value.
use hof
fn main() {
1 5 fn (x:i64 -- r:i64) { 2 * } hof::times print nl // 32 (1*2*2*2*2*2)
2 3 fn (x:i64 -- r:i64) { dup * } hof::times print nl // 256 (2^2=4, 4^2=16, 16^2=256)
}
Practical examples
Data validation
use hof
fn validate_age( age:i64 -- valid:i64 ) {
-> age // bind parameter
// Must be: positive AND >= 18 AND <= 120
age fn (x:i64 -- r:i64) { 0 > } fn (x:i64 -- r:i64) { 18 >= } hof::bi and
age 120 <= and
}
fn main() {
25 validate_age if { "Valid" } else { "Invalid" } print nl
-5 validate_age if { "Valid" } else { "Invalid" } print nl
150 validate_age if { "Valid" } else { "Invalid" } print nl
}
Computing multiple results
use hof
fn stats( n:i64 -- doubled:i64 squared:i64 incremented:i64 ) {
fn (x:i64 -- r:i64) { 2 * }
fn (x:i64 -- r:i64) { dup * }
fn (x:i64 -- r:i64) { 1 + }
hof::tri
}
fn main() {
5 stats
print nl print nl print nl // 6 then 25 then 10
}
Iterative computation
use hof
fn main() {
// Compute 2^10 by doubling 10 times
1 10 fn (x:i64 -- r:i64) { 2 * } hof::times
print nl // 1024
// Compute factorial(5) iteratively
1 -> result
1 6 1 for i {
result i * -> result
}
result print nl // 120
}
Comparison with factor
Quadrate's combinators are inspired by Factor, but with explicit type signatures:
| Factor | Quadrate |
|---|---|
[ 2 * ] |
fn (x:i64 -- r:i64) { 2 * } |
5 [ 2 * ] [ 3 + ] bi |
5 fn (...) { 2 * } fn (...) { 3 + } hof::bi |
The signatures are more verbose but provide compile-time type checking.
Limitations
Quadrate's anonymous functions have some limitations:
- Each must have an explicit type signature
- Variables are captured by reference (changes are visible to the closure)
For more advanced functional patterns, consider using named helper functions. See Anonymous Functions for details on closures and variable capture.
What's next?
Now let's learn about Memory Management.