Looping & Branching
We’ve now seen how commands work with choice types (via selection), and function types (via sending). But those aren’t the only types that come with their own command styles. Every type does.
Let’s turn our attention to something more intricate: recursive types.
Take this familiar definition:
type List<a> = recursive either {
.end!,
.item(a) self,
}
The List
type is recursive, and contains an
either
, and within that, a pair.
To work with such a structure in process syntax, we’ll need to combine three kinds of commands:
.begin
and.loop
for recursion,.case
branching on the either,- And
value[variable]
for receiving (peeling off) a value from a pair.
We’re now going to demonstrate all of these by implementing a string concatenation function.
dec Concat : [List<String>] String
This function takes a list of strings, and returns them concatenated. We’ll use process syntax and see how it works out!
def Concat = [strings] do {
Let’s go step by step!
Recursion in processes
We start by telling the process: “Hey, we’re about to work with a recursive value!”
That’s what .begin
does. It establishes a looping point, to which we can jump back later
using .loop
.
strings.begin
Branching
The .case
command behaves similarly to expression-based .case
, but with two key differences:
In process syntax, the body of each .case
branch is a process, not a value.
This means the syntax requires curly braces after the =>
. You do something in each branch —
not return something.
strings.case {
.end! => {
// empty list, nothing to do
}
.item => {
There’s another key difference: notice the .item
branch has =>
right after the branch name!
There’s no pattern. That’s because in process syntax, binding the payload of an
either is optional. Normally, the subject itself becomes the payload.
However, it is possible to match the payload fully, if desired.
So, inside the .item
branch, strings now has the type (String) List<String>
— it’s a pair, and
we want to peel off its first part, to add it to the string builder.
There’s one last important detail, and that’s concerning control flow. If a .case
branch process does not end (we’ll learn about ending processes in the section about
chan
expressions), then it proceeds to the next line after the closing
parenthesis of the .case
command. All local variables are kept.
Receiving
To peel a value from a pair, we use the receive command:
strings[str]
This command says:
“Take the first part of the pair and store it in str
. Keep the rest as the new value of strings
.”
Now we can assemble the basic skeleton:
def Concat = [strings] do {
let builder = String.Builder
strings.begin.case {
.end! => {
// nothing to do
}
.item => {
strings[str]
builder.add(str)
strings.loop
}
}
} in builder.build
This looks very imperative! The variable strings
flows through the process —
branching, unwrapping, and looping — without needing to be reassigned. It performs all the
different operations based on its changing type. Meanwhile, builder
accumulates the result,
tagging along the control flow of strings
.
Patterns in .case
branches
The .item =>
branch can be made a little more pleasant. If the subject after .begin is a
pair, we’re allowed to use pattern matching directly in the .case
branch.
That means this:
.item => {
strings[str]
builder.add(str)
strings.loop
}
Can be rewritten as:
.item(str) => {
builder.add(str)
strings.loop
}
Which is exactly what we’ll do in the final version:
dec Concat : [List<String>] String
def Concat = [strings] do {
let builder = String.Builder
strings.begin.case {
.end! => {}
.item(str) => {
builder.add(str)
strings.loop
}
}
} in builder.build
def TestConcat = Concat(*("A", "B", "C")) // = "ABC"
This beautifully ties together all the commands we’ve covered so far:
- Selection for choices.
- Sending for functions.
- Branching for eithers.
- Receiving for pairs. Here, in the form of a pattern.
The receive commands is the least clearly useful here. Let’s move to infinite sequences to see a more compelling use-case.