19. Higher-Order Combinators¶
Chapters 17 and 18 gave you function values and closures. This chapter puts them to work with
three classic list combinators — map, filter, and fold — plus compose, all from the
opt-in collections/iter module.
Unlike List and HashMap, these are not always in scope: you bring them in with a use.
use <collections/iter>
collections/iter is the first Sushi module written in Sushi itself — the combinators are
ordinary generic functions that compile alongside your program. Nothing is generated unless you
actually call one, so an unused use costs nothing.
map — transform every element¶
map(xs, f) applies f to each element of a List@(T) and collects the results into a new
List@(U):
use <collections/iter>
fn main() i32:
let i32 crew = 4
let List@(i32) ages = List.new()
ages.push(30)
ages.push(42)
ages.push(200)
# Everyone gains `crew` years while stuck on the Heart of Gold.
let List@(i32) older = map(ages, |i32 a| a + crew).realise(List.new())
println(older.get(1).realise(-1))
return Result.Ok(0)
Output:
46
The lambda |i32 a| a + crew captures the outer local crew (Chapter 18) — combinators and
closures work together with no special ceremony. Here the call is the free-function form,
map(ages, ...), so the list is the first argument; the method form ages.map(...) exists
too, and gets its own section below.
filter — keep the ones that match¶
filter(xs, pred) keeps exactly the elements for which pred returns true:
use <collections/iter>
fn main() i32:
let i32 minimum = 42
let List@(i32) readings = List.new()
readings.push(10)
readings.push(42)
readings.push(99)
readings.push(7)
# Keep only the improbability readings that clear the threshold.
let List@(i32) high = filter(readings, |i32 r| r >= minimum).realise(List.new())
println(high.len())
return Result.Ok(0)
Output:
2
Two of the four readings (42 and 99) clear the threshold, so the filtered list has length 2.
fold — collapse a list to a single value¶
fold(xs, init, f) threads an accumulator through the list left to right, starting from init:
use <collections/iter>
fn main() i32:
let List@(i32) bill = List.new()
bill.push(6)
bill.push(6)
bill.push(30)
# Sum the tab at Milliways, starting from a base cover charge of 0.
let i32 total = fold(bill, 0, |i32 acc, i32 item| acc + item).realise(-1)
println(total)
return Result.Ok(0)
Output:
42
Each step computes acc + item; starting from 0, the tab at Milliways comes to 6 + 6 + 30.
fold is the general shape behind sum, product, min/max, and many other one-value reductions.
A plain function instead of a lambda¶
Any argument that expects a fn(...) value accepts a function reference — just name a
top-level function:
use <collections/iter>
fn negate(i32 x) i32:
return Result.Ok(0 - x)
fn main() i32:
let List@(i32) owed = List.new()
owed.push(5)
owed.push(8)
# Pass a plain function by name -- no lambda needed.
let List@(i32) credited = map(owed, negate).realise(List.new())
println(credited.get(0).realise(0))
return Result.Ok(0)
Output:
-5
compose — glue two functions together¶
compose(nom g, nom f) builds a new function that runs g first, then feeds the result to f:
use <collections/iter>
fn babel(i32 x) i32:
return Result.Ok(x + 1)
fn improbability(i32 x) i32:
return Result.Ok(x * 2)
fn main() i32:
# compose(g, f) runs g first, then f -- here babel, then improbability. Both are `nom`:
# the returned closure captures them, so it becomes their owner.
let fn(i32) -> i32 pipeline = compose(nom babel, nom improbability).realise(improbability)
println(pipeline(20).realise(-1))
return Result.Ok(0)
Output:
42
compose(nom babel, nom improbability) returns a fn(i32) -> i32 that computes
improbability(babel(20)) = (20 + 1) * 2. The returned function is a closure that captures both
babel and improbability — exactly the capture-and-call machinery from Chapter 18, now packaged
for you.
The two nom markers are why: the closure captures both functions, so it becomes their owner,
and compose declares that. map, filter and fold only call their function argument, so
they borrow it and take no marker.
The method form — chain with ??¶
Each combinator also exists as an extension method on List@(T) and on T[]. A
method declares the error channel | StdError, so a call yields a Result — handle
each link with ?? and the chain reads left to right:
use <collections/iter>
fn tab_total() i32:
let List@(i32) orders = List.new()
orders.push(6)
orders.push(6)
orders.push(30)
# Double every order, keep the big ones, and sum what is left.
let i32 total = orders.map(|i32 x| x * 2)??.filter(|i32 x| x > 10)??.fold(0, |i32 acc, i32 x| acc + x)??
return Result.Ok(total)
fn main() i32:
println("{tab_total().realise(-1)}")
return Result.Ok(0)
Output:
84
Leave a ?? out and the compiler stops you with CE2515, spelling the fix: an unhandled
Result has no .filter(), its payload does. On a T[] receiver the collecting
methods return a List — a dynamic array has no empty generic constructor to fill.
Two things to know¶
Element ownership
map and fold work on copyable element types — integers, floats, bool, strings,
and copyable structs. The method-form filter is fully general: it clones each
kept element, so an owning element type works there. The free-function filter
stays copy-only.
Annotate bare-parameter lambdas passed to a combinator
A bare-parameter lambda (|x| ...) cannot infer its type against a generic parameter, since
the combinator's own type parameters are still being solved. Give the parameter a type
(|i32 x| ...) or pass a function reference. To hand a generic function to a combinator,
bind it to a typed local first:
let fn(i32) -> i32 id = identity # fixes the instantiation
let List@(i32) same = map(xs, id).realise(List.new())
What you learned¶
use <collections/iter>brings inmap,filter,fold— as methods onList@(T)andT[]AND as free functions — pluscompose.- The method form declares the
| StdErrorchannel: chain with??(xs.map(f)??.filter(p)??.fold(0, g)??), and CE2515 catches a link you forgot to handle. collections/iteris the first Sushi-source standard-library module; the combinators monomorphize like any generic and cost nothing when unused.- Each combinator takes a
fn(...)value: a lambda (capturing or not) or a plain function reference. composereturns a closure that captures and calls the two functions you give it.mapandfoldare copy-element; the method-formfilterclones and is fully general. Annotate bare-parameter lambdas, or bind a generic function to a typed local, when passing them to a combinator.