Chapter 3 Quotations and Higher-Order Functions
SourceA quotation is a first-class block of code. You write one by wrapping some source in square brackets — [ 2 * ] — and the whole bracketed form becomes a value you can pass around, store in a record, return from a word, and eventually invoke.
If you know Rust, this is a closure. If you know ML or Haskell, it is a lambda. Silo calls it a quotation because that's the name the Forth and Joy families use for the same construct, and the language inherits enough of their idioms that the old name still fits.
This chapter introduces three things, in order:
- How to make a quotation and how to invoke it.
- Its type, its effect row, and the thing that makes Silo quotations especially composable: row polymorphism.
- How higher-order library words like
.map,.filter,.fold, and.for-eachuse quotations to drive ordinary iteration.
Writing and invoking a quotation
A quotation is a block in square brackets:
[ 2 * ]
Pushed on its own, this leaves a quotation value on the stack. To actually run the code inside, call .call:
5 [ 2 * ] .call # ⌊10⌉
Reading it left to right: push 5, push the quotation [ 2 * ], then .call pops the quotation and runs its body — which pops 5, pushes 10. The .call method handles both the invocation and the argument-passing in one step, because the quotation's body operates on the same stack as the caller.
Capturing bindings
Quotations are closures: they capture pop-> and peek-> bindings from the surrounding word. Here's a factory that builds a custom adder:
:fn make-adder ( Int -> [ Int -> Int ] )
pop-> n
[ n + ]
:end
10 make-adder pop-> add10
5 add10 .call # ⌊15⌉
make-adder binds n from its argument, then returns a quotation that references n. When you later .call the quotation, the n is still there — just like a Rust move closure or an ML lambda. You don't need to annotate anything: the compiler works out what got captured.
The type of a quotation
The signature [Int -> Int] in the example above is the quotation type. It has the same shape as a word's stack effect: [inputs -> outputs +effects]. So [Int -> Int] is a quotation that pops an Int and pushes an Int, with no declared effects.
Quotations can carry effects just like words can:
[ "{}" format print ] # type: [Int -> +Console]
When a higher-order word is given an effectful quotation, the caller inherits those effects — covered in the effects chapter.
Row polymorphism
Here's the part that deserves a pause.
The quotation [ 2 * ] reads as "pop an Int, push an Int". But what actually happens to the rest of the stack? It stays untouched — [ 2 * ] doesn't care what's below the Int on top.
Silo captures this formally in the quotation's type. The full type of [ 2 * ] is:
[..s Int -> ..s Int]
The ..s is a stack row variable. It stands for "whatever was below the top of the stack, unchanged." Every quotation is implicitly row-polymorphic over its untouched stack tail.
Row polymorphism is why [ 2 * ] works equally well as an argument to .map, as a standalone invocation on a stack with one Int on it, or as part of a longer pipeline where many values sit beneath it. You don't have to declare the row variable. You don't even have to know it's there. But it's what makes Silo's composition feel natural rather than ceremonial — a quotation that operates on the top two values composes with any stack shape that has those two values on top.
If you've written Haskell, the closest analogue is the stack-type-variable machinery that Factor's and Kitten's type systems use. If you haven't, the short version is: quotations compose like Unix pipes. What's below flows through.
Higher-order functions
Silo's library takes quotations as arguments everywhere it makes sense. The most common ones are methods on Foldable and on Iterator — .map, .filter, .fold, .for-each, .reduce, .flat-map, .take, .drop, and so on. They're covered in the collections chapter; here's a taste:
1 2 3 4 5 vec/5
[ 2 * ] .map # [2 4 6 8 10]
[ 5 > ] .filter # [6 8 10]
0 [ + ] .fold # 24
Every one of these takes a quotation plus (for .fold) an initial value, and produces a transformed collection or a scalar. .for-each is similar but throws away the result — useful when the quotation is there for its side effects:
1 2 3 vec/3 [ "{}" format print ] .for-each
.map/.filter/.fold are eager. The lazy, element-at-a-time Iterator protocol is a sibling trait — reach for it when you want short-circuiting or infinite sequences. The mechanics are in the collections chapter again.
Collecting a quotation into a Vec
A quotation that pushes multiple values can be materialised into a Vec with .collect. This is how you build most literal Vec values in practice:
[ 1 2 3 4 5 ] .collect # (Vec Int | 5)
[ "a" "b" ] .collect # (Vec Str | 2)
.collect relies on a feature of the stack-effect system called output variadics. The quotation's body pushes values; the compiler counts them statically; .collect then gathers them into a Vec of exactly that length. The length ends up in the Vec's type, so a typo that produces too many or too few elements is caught at compile time.
Output variadics have their own chapter (Variadics and Reflection). For now, think of [ … ] .collect as the idiomatic way to produce a Vec literal.
Key points
[ … ]is a first-class closure. It captures surroundingpop->andpeek->bindings; the compiler works out the capture set automatically..callinvokes a quotation against the current stack.- Quotation types have the same shape as word signatures:
[inputs -> outputs +effects]. - Every quotation is row-polymorphic over its untouched stack tail. That's why
[ 2 * ]composes with.map, with.fold, and with direct use, without any extra annotations. - Higher-order words like
.map,.filter,.fold, and.for-eachare ordinary library functions that accept quotations as arguments. [ … ] .collectis the idiomatic way to build aVecliteral; it uses output variadics so the length is compile-time known.
The next chapter introduces records, unions, enums, and the other named data types.