Chapter 21 Complete Examples
SourceFour worked examples that pull together most of the language. Each is a real compilable program; read them front to back to see how the pieces fit.
FizzBuzz
The classic. Iterates a range, matches on divisibility, and prints each line.
:fn fizzbuzz ( Int -> ) + Console
pop-> count
0 .. count [ pop-> i
i 1 + pop-> n
n 15 mod 0 = :if
"FizzBuzz" print
:else
n 3 mod 0 = :if
"Fizz" print
:else
n 5 mod 0 = :if
"Buzz" print
:else
n "{}" format print
:end
:end
:end
] .for-each
:end
:main + Console
20 fizzbuzz
:end
Points worth noting:
0..countis aRangethat the standard library treats asFoldable/Iterator, so.for-eachworks directly.- The quotation
[ pop-> i … ]capturescountimplicitly — it's a closure. - All the arithmetic goes through ordinary trait methods (
mod,=,+).:if/:elsechains handle the branching.
Binary search
Runs on an existential-length (Vec Int). Declares +Div because the :loop break condition is runtime-dependent — the compiler can't prove termination statically.
:fn binary-search ( ( Vec Int ) Int -> ( Option AnyUInt ) ) + Div
pop-> target pop-> vec
0 pop-> lo
vec .len pop-> hi
:loop
lo hi ≥ :if None :break :end
lo hi + 2 / pop-> mid
mid vec .get .unwrap pop-> val
val target = :if
mid Some :break
:end
val target < :if
mid 1 + pop-> lo
:else
mid pop-> hi
:end
:end
:end
The return type (Option AnyUInt) says "either an index, or none." If the loop breaks with None, the search failed; if it breaks with mid Some, we found the target at position mid.
Temperature converter
Two newtype records — Celsius and Fahrenheit — plus From impls that convert between them, plus Display impls for locale-neutral rendering. The newtypes make sure you can't accidentally pass a Celsius where a Fahrenheit is expected.
:record Celsius .value F64 :end
:record Fahrenheit .value F64 :end
:impl From Fahrenheit Celsius
.from ( Celsius -> Fahrenheit )
.value 9.0 * 5.0 / 32.0 + Fahrenheit ;
:end
:impl From Celsius Fahrenheit
.from ( Fahrenheit -> Celsius )
.value 32.0 - 5.0 * 9.0 / Celsius ;
:end
:impl Display Celsius
.fmt ( Celsius FmtSpec -> Str )
drop .value "{:.1}°C" format ;
:end
:impl Display Fahrenheit
.fmt ( Fahrenheit FmtSpec -> Str )
drop .value "{:.1}°F" format ;
:end
:main + Console
100.0 Celsius pop-> boiling
boiling "{}" format print # "100.0°C"
boiling ( Fahrenheit .from) pop-> f
f "{}" format print # "212.0°F"
f ( Celsius .from) "{}" format print # "100.0°C"
:end
Notes:
(Fahrenheit .from)is qualified dispatch — it resolves to the specificFrom Fahrenheit Celsiusimpl.- The
FmtSpecargument to.fmtcarries width/align/fill; here the impl drops it because it doesn't honour any layout beyond the trait-specific{:.1}precision. A richer impl would consultFmtSpecto decide whether to pad. Fromauto-generates the reverseInto, so.intowould also work at the call site if a target-typed context were available.
Simple logger with aspects
The longest example — an aspect-driven logger with a concrete handler, invoked through :with. Brings together unions, aspects, :impl, and scoped handler installation.
:union LogLevel
| LTrace | LDebug | LInfo | LWarn | LError
:end
:aspect Log
.log ( Str LogLevel -> ) drop drop ;
:end
:record StdoutLogger :end
:impl Log StdoutLogger
.log ( Str LogLevel -> ) + Console
pop-> level
pop-> msg
level :match
| LTrace => "TRACE"
| LDebug => "DEBUG"
| LInfo => "INFO"
| LWarn => "WARN"
| LError => "ERROR"
:end pop-> label
label msg "[{}] {}" format print ;
:end
:use
:aspect logging Log
:end
:fn process-items ( ( Vec Str ) -> ) + Console
"starting batch" LInfo .log
[ pop-> item
item "processing: {}" format LDebug .log
] .for-each
"batch complete" LInfo .log
:end
:main + Console
StdoutLogger :with Log
"a" "b" "c" vec/3 process-items
:end
:end
Output:
[INFO] starting batch
[DEBUG] processing: a
[DEBUG] processing: b
[DEBUG] processing: c
[INFO] batch complete
A few things worth teasing apart:
- The
.logsignature( Str LogLevel -> )has the level on top of the stack — you write"msg" LInfo .log, with the level as the "receiver-like" value closest to the call. - The default body
drop drop ;makes the call a no-op when no handler is installed. That's why.loghas no+Logeffect in the signatures that call it: the aspect system doesn't need one. StdoutLogger :with Log … :endinstalls the handler for the body's dynamic extent. Inside, every.logdispatches toStdoutLogger's impl; outside, the default drops.- The
:aspect logging Logimport in:usebrings the aspect's operation (.log) into scope as an ordinary method — no+Logannotation anywhere inprocess-items.
Where to go from here
If you've read the book straight through, you've seen every feature the language ships with at least once. A reasonable set of next steps:
- Build something small against a CLI host — a line-counter, a config parser, a toy HTTP client. The host-capability story clicks quickly once you have to declare the effects your
:mainneeds. - Open the
silo:stdsource and read through the trait implementations for the primitives. The standard library is deliberately written in Silo (not baked into the host) so that its idioms demonstrate how to use the language in production. - Reach for a non-trivial dependent-type pattern — a length-preserving function, a refined-integer port number, a phantom-tagged unit — and work it until the type error becomes legible rather than intimidating.
- When you want to go deeper on a specific topic, the specification (
spec/*.md) is the authoritative source. Every rule is numbered (si[...]) and cross-referenced; the book points at specific rules where a subtle decision merits it.
The glossary is an index if you want to jump between concepts; the book root has a table of contents for browsing.
Thanks for reading.