17. First-Class Functions¶
So far a function has been something you call. In this chapter a function becomes something you
can hold: store it in a variable, pass it to another function, keep a whole List of them, and
call through any of those. That one idea — functions as values — is what turns a pile of if/
match dispatch into clean, data-driven code.
This builds on Chapter 4 (Functions), Chapter 6 (Error Handling), and Chapter 13 (Collections). If you know Python's "functions are objects" or C's function pointers, you already have the intuition — Sushi's version is typed and compiles to a bare pointer with zero overhead.
A function as a value¶
Write a function's name without parentheses and you get a function value — a reference to
that function. Its type is a function type, written fn(params) -> return:
# A function value: reference a named function, pass it to another function,
# and call through the parameter. A plain reference like this carries no
# captured state -- see chapter 18 for closures, which do.
fn add_one(i32 x) i32:
return Result.Ok(x + 1)
fn triple(i32 x) i32:
return Result.Ok(x * 3)
# `op` is a function-typed parameter; `op(v)` calls through it.
fn apply(fn(i32) -> i32 op, i32 v) i32:
return Result.Ok(op(v)??)
fn main() i32:
let fn(i32) -> i32 f = add_one
let fn(i32) -> i32 g = triple
println(apply(f, 41).realise(0)) # 42
println(apply(g, 14).realise(0)) # 42
return Result.Ok(0)
Output:
42
42
Three things to notice:
let fn(i32) -> i32 f = add_onebinds the function value. The type reads "takes ani32, returns ani32" — the same shape asadd_one's signature.apply(fn(i32) -> i32 op, i32 v)takes a function as a parameter. Inside,op(v)calls through it.- Calling through a function value returns a
Resultjust like calling the function directly, so the familiar??works onop(v).
A plain reference vs. a closure
A plain function reference like add_one above carries no captured variables — it's the bare
address of the compiled function, with no allocation and no cleanup. Sushi also has
closures: a lambda literal that does capture surrounding variables, covered
in the next chapter. Both are fn(...)-typed values with identical call syntax.
A dispatch table with List¶
Because a function value is an ordinary value, you can put a bunch of them in a List and iterate
— a dispatch table or pipeline:
# A List<fn(...)> is a dispatch table you can iterate. Each step transforms the
# accumulator by calling through the stored function value.
fn add_one(i32 x) i32:
return Result.Ok(x + 1)
fn add_two(i32 x) i32:
return Result.Ok(x + 2)
# steps is borrowed (&peek) so the caller keeps the table and can free it below; a
# by-value List<...> would be moved into run_pipeline and freed there.
fn run_pipeline(&peek List<fn(i32) -> i32> steps, i32 start) i32:
let i32 acc = start
foreach(step in steps.iter()):
acc := step(acc)??
return Result.Ok(acc)
fn main() i32:
let List<fn(i32) -> i32> steps = List.new()
steps.push(add_one)
steps.push(add_two)
steps.push(add_one)
println(run_pipeline(&peek steps, 10).realise(0)) # 10 +1 +2 +1 = 14
steps.free()
return Result.Ok(0)
Output:
14
List<fn(i32) -> i32> is a list whose element type is a function type. foreach hands you each
stored function in turn, and step(acc) calls through it. (Use List<fn(...)> rather than a raw
array for a collection of functions — in fn() -> T[] the [] belongs to the return type T[],
so there is no "array of functions" syntax.)
Functions in a struct¶
A function value can be a struct field — handy for bundling a name, some config, and the behavior to run:
# A function value can live in a struct field, and you can call it directly:
# `op.run(v)` routes to the fn-typed field when there is no method of that name.
fn square(i32 x) i32:
return Result.Ok(x * x)
struct Op:
string name
fn(i32) -> i32 run
fn apply(Op op, i32 v) i32:
return Result.Ok(op.run(v)??)
fn main() i32:
let Op op = Op("square", square)
println(op.name)
println(apply(op, 7).realise(0)) # 49
return Result.Ok(0)
Output:
square
49
You can call the field directly — op.run(7). When a struct has a fn-typed field and no
method of the same name, op.run(7) routes to the function stored in the field. (If a method
run also existed, the method would win; bind the field to a local first — let f = op.run — to
call the field in that case.)
The error type travels with the function¶
A function type can spell out a custom error type, just like a declaration does with | E. That
error type is part of the type, so it propagates correctly through an indirect call:
# The error type is part of the function type, so a custom error propagates
# through an indirect call exactly like it would through a direct one.
enum DivError:
DivByZero
fn safe_div(i32 a, i32 b) i32 | DivError:
if (b == 0):
return Result.Err(DivError.DivByZero)
return Result.Ok(a / b)
fn run(fn(i32, i32) -> i32 | DivError op, i32 x, i32 y) i32 | DivError:
let i32 r = op(x, y)??
return Result.Ok(r)
fn main() i32:
let fn(i32, i32) -> i32 | DivError f = safe_div
println(run(f, 84, 2).realise(-1)) # 42
println(run(f, 1, 0).realise(-1)) # -1 (DivByZero -> realise default)
return Result.Ok(0)
Output:
42
-1
fn(i32, i32) -> i32 | DivError says the function can fail with a DivError. Inside run, the
op(x, y)?? propagates that error out, and the caller turns it into a default with .realise(-1).
Omit the | E and the error type is the implicit StdError, exactly as for a normal fn.
Calling through any expression¶
A function value doesn't have to sit in a plain variable to be called. You can call through any
expression that produces one — a List element or a parenthesized expression:
let List<fn(i32) -> i32> table = List.new()
table.push(add_one)
let i32 a = table.get(0)??(41)?? # call the retrieved function value
let i32 b = (table.get(0)??)(41)?? # same, parenthesized
Referencing a generic function¶
A generic function can be referenced as a value when you give the binding an explicit function type — the annotation fixes which instantiation you mean:
fn identity<T>(T x) T:
return Result.Ok(x)
let fn(i32) -> i32 g = identity # identity<i32>, chosen by the annotation
let i32 n = g(41)?? # 41
Without an expected function type — for instance passing identity straight into a call argument
with no typed binding — the reference is still CE2093; bind it to a typed local first.
What else the compiler checks¶
- A wrong-shaped call through a function value (wrong arity or argument type) → CE2092.
- Assigning a function value to an incompatible function-typed variable → CE2002. Function types are invariant: arity, every parameter, the return type, and the error type must match exactly.
Extension methods, perk methods, and C externals aren't bare-referenceable at all — they have different calling conventions, so a bare name that isn't a plain function is just an undeclared identifier.
What you learned¶
- A function value is a function's name used without
()— its type is a function typefn(params) -> return [| Error]. - You can store function values (variables, struct fields,
List<fn(...)>), pass them as arguments, and call through them; an indirect call returns aResultjust like a direct one. - A plain function reference is a bare pointer — zero-cost, no captured state. Sushi also has closures (capturing lambda literals) — see the next chapter.
- Call a function-valued struct field directly (
obj.field(x)); a same-named method would win. You can also call through any expression that yields a function value (table.get(0)??(x)). - Reference a generic function as a value when an explicit function type is present
(
let fn(i32) -> i32 g = identity); a bare reference with no expected type is CE2093. - The error type is part of the function type and propagates through
??. - A call-through mismatch is CE2092, and an assignment mismatch is CE2002.
That's functions-as-data. Next, Chapter 18 (Closures) adds the capturing lambda literal. For the complete reference on this chapter's material, see the First-Class Functions guide and the design note.