README.md (13552B)
FizzBuzz, one tree at a time
If you have written FizzBuzz in Python, you already know the goal: count from 1 onward, replacing multiples of three with “Fizz,” multiples of five with “Buzz,” and multiples of both with “FizzBuzz.”
Here we will build that sequence using two repeating clocks. Along the way, you will learn how SCP matches trees, carries state, and produces a program that you can prepare once and run repeatedly. If you would like to try the examples now, skip ahead to preparing a program.
1. A language made of trees
tricu implements tree calculus, where both data and executable programs are trees. SCP gives us a way to write computations over those trees using patterns and rules. We can take a tree apart, name its pieces, and construct another tree from them. Because code has the same representation as data, programs can also inspect and assemble executable trees.
There are three tree shapes:
- Leaf: the smallest tree, written
tin tricu. - Stem: a node with one child.
- Fork: a node with two children.
A pair (left, right) is a Fork. A list is a chain of Forks, each holding an
item and the remaining list, ending in Leaf. For example:
[1, 2, 3] = (1, (2, (3, Leaf)))
Numbers use a compact tree encoding. Strings are lists of byte-number trees. Zero, an empty list, and an empty string all use Leaf. The program consuming a tree supplies its meaning: our FizzBuzz consumer expects a list as input and a pair of clocks as state.
Our finished FizzBuzz program will produce:
ok: [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]
The list is our answer. ok: tells us the invocation returned a successful
Result; a failed computation displays error: followed by its detail.
Let's build up the computation, beginning with two small consumers.
2. Give a tree a name
A consumer receives input and state. Its rules describe how to match those two values and what to produce. This tiny consumer returns its input:
!export identity
consumer identity
value @ _ => value
Read the rule as: “Capture the input as value, accept any state, and return
value.”
- A name such as
valuecaptures a tree for use in the rule's output. @separates the input pattern from the state pattern._accepts any tree and leaves it unnamed.=>separates the patterns from the output.!export identitymakes this entry available to callers by name.
For example, identity returns [1, 2, 3] when given [1, 2, 3], and
returns "hello" when given "hello".
3. Walk a list
We can distinguish an empty list from a list with a head and tail:
!export copyList
consumer copyList
[] @ _ => []
[n | rest] @ state => [n | recur rest @ state]
Rules are tried from top to bottom. [] matches Leaf and returns an empty
list. [n | rest] matches a Fork: n names its head and rest names its tail.
state captures the state.
The output [n | ...] constructs a list cell. To obtain its tail,
recur rest @ state continues copyList on the remaining input,
with the state explicitly supplied. Each step consumes one cell:
input [1, 2, 3] → keep 1, continue on [2, 3]
input [2, 3] → keep 2, continue on [3]
input [3] → keep 3, continue on []
input [] → finish with []
The result is a fresh list with the original values. FizzBuzz will use the same traversal, choosing a label for each head and updating the clocks for the next step.
4. Derive the two clocks
In Python, a common solution tests each number's divisibility. We can also track our position in a repeating cycle. Each clock counts how many advances remain until its next labelled position. Zero means “label this item.” We inspect the clocks before processing an item, then advance them.
The three-clock starts like this:
position 1: 2 keep the item
position 2: 1 keep the item
position 3: 0 say Fizz
position 4: 2 keep the item
Why start at 2? Position 3 is two advances away from position 1.
Why reset to 2? After labelling position 3, we advance to position 4.
The next Fizz is at position 6, two advances away. The clock follows
2 → 1 → 0 → 2, repeating every three items.
The five-clock follows the same reasoning. Position 5 is four advances away
from position 1, so it starts at 4. Its cycle is
4 → 3 → 2 → 1 → 0 → 4.
Together, the state is (three, five):
| Position | State before processing | Output |
|---|---|---|
| 1 | (2, 4) |
1 |
| 2 | (1, 3) |
2 |
| 3 | (0, 2) |
Fizz |
| 4 | (2, 1) |
4 |
| 5 | (1, 0) |
Buzz |
| 6 | (0, 4) |
Fizz |
| 10 | (2, 0) |
Buzz |
| 14 | (1, 1) |
14 |
| 15 | (0, 0) |
FizzBuzz |
These clocks follow positions in the input. A consecutive list starting
at 1, with state (2, 4), gives conventional FizzBuzz. Feeding the same clocks
[100, 200, 300] produces [100, 200, "Fizz"]: the third position receives
the label. To begin a consecutive sequence elsewhere, choose clocks that align
with its starting point. Starting at 15, for example, uses (0, 0).
5. Assemble FizzBuzz
Here is the complete computation from fizzbuzz.scp:
!export fizzbuzz
consumer fizzbuzz
[] @ _ => []
[n | rest] @ (three, five) => [
match (three, five) of (
(0, 0) => "FizzBuzz"
| (0, _) => "Fizz"
| (_, 0) => "Buzz"
| _ => n)
| recur rest @ (
match three of (0 => 2 | 1 => 0 | 2 => 1),
match five of (0 => 4 | 1 => 0 | 2 => 1 | 3 => 2 | 4 => 3))]
The outer shape is our list-copying consumer. The head now comes from a
match, and recurrence receives updated state.
match value of (...) computes a value and tries its arms in order. For the
head, we match the clock pair:
- Both zero: produce
"FizzBuzz". - Three-clock zero: produce
"Fizz". - Five-clock zero: produce
"Buzz". - Otherwise: keep
n.
The combined case comes first because (0, 0) also matches (0, _) and
(_, 0). Moving the Fizz arm first would make position 15 say only Fizz.
The two smaller matches implement the clock cycles as finite tables. For
example, match three of (0 => 2 | 1 => 0 | 2 => 1) resets zero to two and
advances the other phases. The pair of results becomes the state for rest.
Trace position 3: state (0, 2) chooses "Fizz"; the updates produce (2, 1);
recurrence processes position 4 with those clocks. The empty rule ends the
traversal and supplies the final Leaf of the output list.
6. Prepare code with its starting state
Our computation accepts clocks on every step. Configuration chooses their initial values and combines them with the compiled entry to make a worker. That lets us reuse the same code with different starting points.
From the repository root, with Nix and GNU timeout available, after setting up
SCP_BOOTSTRAP as described in the SCP guide:
nix build .#
result/bin/tricu scp prepare demos/consumers/fizzbuzz/fizzbuzz.scp-project \
--bootstrap "$SCP_BOOTSTRAP" --store /tmp/fizzbuzz/cas --output /tmp/fizzbuzz/program.prepared --seconds 240
result/bin/tricu scp run /tmp/fizzbuzz/program.prepared \
--input-tree-file demos/consumers/fizzbuzz/fizzbuzz-input.tree --seconds 60
--seconds sets the time allowed for each launched phase; increase it if a
phase needs more time on your machine, up to 1200 seconds. To construct a project for identity
or copyList, select its source file in the workspace and use that consumer's
name for the project's binding and invocation. Both can use Leaf as initial
state: {"leaf": true}. Let's follow how those selections work for FizzBuzz.
The configuration in fizzbuzz.scp-project says:
"configuration": {
"bind": "fizzbuzz",
"state": {"tree-file": "fizzbuzz-phases.tree"}
}
bind selects the compiled entry by name. state supplies the tree to use
at the start of each invocation: our pair (2, 4). The shared configuration
operation combines them into a worker and publishes it under the invocation
name selected by the project. fizzbuzz-phases.tree contains the serialized
pair (2, 4): the initial three-clock and five-clock. It is generated from the
phases definition in fizzbuzz-data.tri using tricu tree write.
fizzbuzz-input.tree similarly holds the runtime list 1 through 15.
There are two data choices to keep straight:
- Initial state:
(2, 4), bound to the code during preparation. - Runtime input: the list of items, supplied each time we run it.
fizzbuzz-workspace.tri selects our source file.
The project brings together that workspace, initial state, and invocation name.
Its scp-invocation.result.v1 profile selects direct invocation returning a
Result. For configurations that compute state or assemble several workers,
projects can instead select an ordinary SCP configuration consumer; the
SCP guide shows both forms.
When you run prepare, the workflow:
- Compiles and publishes the reusable entries.
- Binds the chosen initial state and publishes the worker and its Result-returning invocation.
- Checks the selected invocation name and writes the prepared record.
The published trees live in a content-addressed store, or CAS. Each object has a pin: a hash identifying its contents. A manifest maps exported names to their objects; receipts record the build and configuration inputs. The prepared record selects those artifacts and the entry to invoke.
run loads that selection and applies the supplied input. Each invocation
starts with the configured clocks. To select a source edit or new initial
state, prepare again; an existing record continues to select its original
program. Runtime input can change on every invocation.
For deployment, move the record and CAS together, preserving their relative layout, and run them with tricu and the launcher. The build's source files can stay behind. The SCP guide also shows the separate phase commands for building code once and configuring it several ways.
7. Make the data your own
The .tree files hold serialized trees. To write new values, edit the two
named definitions in fizzbuzz-data.tri:
phases = pair 2 4
input = [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15]
Select each definition to serialize it through the existing tree codec:
result/bin/tricu tree write demos/consumers/fizzbuzz/fizzbuzz-data.tri \
--name phases --output demos/consumers/fizzbuzz/fizzbuzz-phases.tree --store /tmp/fizzbuzz/data-authoring
result/bin/tricu tree write demos/consumers/fizzbuzz/fizzbuzz-data.tri \
--name input --output demos/consumers/fizzbuzz/fizzbuzz-input.tree --store /tmp/fizzbuzz/data-authoring
tree write evaluates the selected definition and writes its exact tree bytes.
This is the explicit authoring step. During invocation, the argument loader
decodes those bytes. The .tri list syntax separates items with spaces; SCP
lists use commas.
Back up your files and try these experiments one at a time. Restore the original data and source between experiments.
| Edit | Next steps | Expected outcome |
|---|---|---|
Runtime list becomes [100 200 300] |
Regenerate data, run | [100, 200, "Fizz"] |
Runtime list becomes [] |
Regenerate data, run | ok: [] |
Phases become pair 0 0 |
Regenerate data, prepare, run the original list | Begins "FizzBuzz", 2, 3, "Fizz", 5, "Buzz" |
| Three-clock becomes 3 | Regenerate data, prepare, run nonempty input | Failure: no matching output arm |
Runtime input becomes pair 7 (t t) |
Regenerate data, run | Failure: no matching consumer rule |
Move (0, _) before (0, 0) in source |
Prepare, run | Position 15 says "Fizz" |
Change "Fizz" to "Fizz!" in source |
Run the old record, then prepare and run | The new record selects the changed label |
The two failures explain the program's patterns. Three-clock 3 falls outside
its update table. The pair pair 7 (t t) supplies a head, but its Stem tail
misses both the empty-list and list-cell rules. In each case the invocation
returns a failure Result with the message shown above.
The Result display presents the outcome and payload separately: ok: [] for
an empty answer, or error: no matching output arm for an invalid phase.
To examine the complete underlying tree, choose the tree view:
result/bin/tricu scp run /tmp/fizzbuzz/program.prepared \
--input-tree-file demos/consumers/fizzbuzz/fizzbuzz-input.tree --format tree --seconds 60
For empty input it displays t (t t) (t t t): a Fork holding the success
marker and a pair whose payload and remainder are both Leaf. The display
choices preserve that same Result tree.
Continue exploring
You have now matched trees, traversed a list, derived explicit state, and combined compiled code with data for repeated invocation. The small lessons take the next step: inspecting executable trees and comparing their representations. The SCP guide covers reusable workers and larger applications; the language contract gives precise matching and control semantics.