Channels & Linking
In the previous sections, we built up values using the do
/in
expressions:
def Message = do {
let builder = String.Builder
builder.add("Hello")
builder.add(", ")
builder.add("World")
} in builder.build
This form lets you run a process — a sequence of commands — and then return a final expression at the end.
But what if you want to return early, based on some logic? Par has no return keyword. But it does have a deeper mechanism that allows exactly this — and more.
The do
/in
syntax is just sugar for something more fundamental: the chan
expression,
combined with a command called linking. This:
do {
<process>
} in <value>
is exactly the same as:
chan result {
<process>
result <> <value>
}
Let’s unpack what this means.
The chan
Expression
chan inner {
// commands here
}
This expression creates a new channel, and spawns a process that runs the commands inside the
curly braces. That process is given access to one endpoint of the new channel —
named here as inner
.
The chan
expression itself evaluates to the other endpoint of the channel — that’s the value
the expression returns.
The key is this: If the whole chan expression has type T
, then inside the block, inner
has type dual T
.
We’ll explore the details of dual types in the next section, but for now, here’s the intuition: A type and its dual are two opposite side of communication. If one side offers something, the other one requires it, and vice versa.
For example, if the whole chan
expression should evaluate to an Int
, then dual Int
is
a requirement of an integer.
Linking — the <>
command
The link command is the simplest way to fulfill that requirement. If x
has type T
and y
has type dual T
, then:
x <> y
Connects the two endpoints, and ends the process.
For example:
def FortyTwo: Int = chan out {
out <> 42
}
This returns the number 42
, using a full process.
Like all commands, <>
operates on values — here, two channels — not processes.
You’re not linking a process to a value. You’re linking one channel to another.
Processes in
chan
must end. Every process inside achan
must end with one of two commands:
- A link command (x <> y), or
- A break command (x!), which we’ll meet in the next section.
These are the only ways a process can terminate. If you reach the end of a
chan
block without either one, it’s an error. This may seem a strange condition, but it’s important for some concurrency invariants Par provides.In contrast,
do
/in
blocks don’t end themselves — they continue into the expression afterin
.
Let’s write a function that can use that early return.
It takes a natural number (Nat
) and returns a String
.
- If the number is zero, it returns the string
"<nothing>"
. - Otherwise, it returns a string of exactly
n
hash marks ("#"
).
Here’s how that looks:
dec HashesOrNothing : [Nat] String
def HashesOrNothing = [n] chan result {
Nat.Equals(n, 0).case {
.true! => {
result <> "<nothing>"
}
.false! => {}
}
let builder = String.Builder
Nat.Repeat(n).begin.case {
.end! => {}
.step remaining => {
builder.add("#")
remaining.loop
}
}
result <> builder.build
}
Let’s break this down:
Nat.Equals(n, 0)
returns aBool
, which is aneither
with branches.true!
and.false!
.- In the
.true!
branch, we immediately perform a link — returning"<nothing>"
. Note, that this ends the process. - The
.false!
branch does nothing on its own — the real work happens after the.case
. - We build the result using a
String.Builder
, looping exactlyn
times usingNat.Repeat
. - Finally, we link the builder’s result to
result
, ending the process.
In the next section, we’ll learn how to exploit duality, to not just return, but sequentially build on the opposing channels!