24. Coroutines
Coroutines are functions that can suspend execution and yield values one at a time. They are useful for lazy sequences, generators, and any scenario where you want to produce a stream of values on demand without computing them all up front.
24.1 Defining Coroutines
A coroutine is declared with coro fn. The return type is the type of
values it yields. Use yield to produce values.
-- A coroutine that yields integers 0..n-1
coro fn count(n Int) Int {
var i = 0;
while (i < n) {
yield i;
i = i + 1;
}
}
-- A coroutine with multiple explicit yield points
coro fn traffic_light() String {
yield "red";
yield "yellow";
yield "green";
}
yield can only appear in the lexical body of a
coro fn. A regular function called from within a coroutine cannot yield on
the coroutine's behalf. Use yieldAll to delegate to an inner coroutine.
24.2 Resuming with next
Calling a coroutine function creates a Coroutine<T> object.
Use next to resume it and get the next yielded value wrapped in
Option<T>. When the coroutine is exhausted, next
returns none.
let c = 3 count;
c next println; -- Option.some(0)
c next println; -- Option.some(1)
c next println; -- Option.some(2)
c next println; -- Option.none
24.3 Yield Forms
There are three equivalent ways to yield a value.
coro fn mixed_yield() String {
yield "prefix"; -- expression form
"pipeline" yield; -- pipeline form
yield("explicit"); -- call form
}
let m = mixed_yield();
m next println; -- Option.some(prefix)
m next println; -- Option.some(pipeline)
m next println; -- Option.some(explicit)
24.4 Coroutine State and Exhaustion
Each coroutine maintains its own local state across suspensions.
Once finished, every subsequent call to next returns none.
-- State is preserved between yields
coro fn running_sum(n Int) Int {
var sum = 0;
var i = 1;
while (i <= n) {
sum = sum + i;
yield sum;
i = i + 1;
}
}
let rs = 4 running_sum;
rs next println; -- Option.some(1)
rs next println; -- Option.some(3)
rs next println; -- Option.some(6)
rs next println; -- Option.some(10)
rs next println; -- Option.none
rs next println; -- Option.none (stays exhausted)
-- An empty coroutine (no yields) is immediately done
coro fn empty() Int {}
empty() next println; -- Option.none
24.5 For Loops over Coroutines
Coroutines work directly with for loops. The loop variable
receives unwrapped values (not Option), and the loop ends when
the coroutine is exhausted.
for (x : count(5)) {
x println;
}
-- Output: 0 1 2 3 4
-- Works with any yield type
coro fn colors() String {
yield "red";
yield "green";
yield "blue";
}
for (c : colors()) {
c println;
}
-- Output: red green blue
24.6 Infinite Coroutines
Coroutines can yield indefinitely. Use break in a
for loop to stop early.
-- Infinite Fibonacci sequence
coro fn fibs() Int {
var a = 0;
var b = 1;
while (true) {
yield a;
let tmp = a + b;
a = b;
b = tmp;
}
}
-- Take the first 8 values
let f = fibs();
var i = 0;
while (i < 8) {
f next println;
i = i + 1;
}
-- Option.some(0), Option.some(1), Option.some(1), Option.some(2),
-- Option.some(3), Option.some(5), Option.some(8), Option.some(13)
-- Or use for with break
for (x : fibs()) {
if (x > 10) { break; }
x println;
}
-- Output: 0 1 1 2 3 5 8
24.7 Yield Delegation with yieldAll
yieldAll drains an inner coroutine, forwarding each of its values
as a yield from the outer coroutine. The inner coroutine's yield type must match
the outer's.
coro fn inner() Int {
yield 10;
yield 20;
yield 30;
}
coro fn outer() Int {
yield 1;
yieldAll(inner()); -- delegates: yields 10, 20, 30
yield 99;
}
for (x : outer()) {
x println;
}
-- Output: 1 10 20 30 99
-- Pipeline form works too
coro fn outer2() Int {
yield 1;
inner() yieldAll;
yield 99;
}
24.8 Converting to Lists with toList
toList converts a coroutine into a (lazy) list, enabling all list
operations. For infinite coroutines, use take or collect
to bound the result.
-- Finite coroutine to list
5 count toList println;
-- List(0, 1, 2, 3, 4)
-- Infinite coroutine with take
fibs() toList take(8) println;
-- List(0, 1, 1, 2, 3, 5, 8, 13)
-- List operations on the result
5 count toList head println; -- 0
5 count toList tail println; -- List(1, 2, 3, 4)
5 count toList drop(3) println; -- List(3, 4)
-- Map over a coroutine-turned-list
3 count toList map(fn(x Int) Int = x * 10) println;
-- List(0, 10, 20)
-- collect eagerly evaluates the first N elements
fibs() toList collect(6) println;
-- [0, 1, 1, 2, 3, 5]
24.9 The Coroutine Type
The type of a coroutine object is Coroutine<T>, where T
is the yield type. This can be used in type annotations and as function parameter types.
-- Explicit type annotation
let c Coroutine<Int> = 3 count;
-- Function that drains any Coroutine<Int>
fn drain(c Coroutine<Int>) Void {
var result = c next;
while (result isSome) {
result println;
result = c next;
}
}
4 count drain;
-- Option.some(0) Option.some(1) Option.some(2) Option.some(3)
24.10 Composition Patterns
Coroutines can yield any type including tuples, arrays, structs, and enums. They also compose well: a coroutine can create and drive other coroutines.
-- Yielding tuples
coro fn pairs(n Int) (Int, Int) {
var i = 0;
while (i < n) {
yield (i, i * i);
i = i + 1;
}
}
for (p : 4 pairs) {
p println;
}
-- (0, 0) (1, 1) (2, 4) (3, 9)
-- Yielding arrays
coro fn make_arrays() [Int] {
yield [1, 2, 3];
yield [4, 5];
yield [6];
}
for (a : make_arrays()) {
a println;
}
-- [1, 2, 3] [4, 5] [6]
-- Yield from conditionals and match
coro fn conditional(flag Bool) Int {
if (flag) {
yield 1;
yield 2;
} else {
yield 100;
yield 200;
}
yield 999;
}
true conditional toList println; -- List(1, 2, 999)
false conditional toList println; -- List(100, 200, 999)
-- Composing: one coroutine driving another
coro fn doubled(c Coroutine<Int>) Int {
for (x : c) {
yield x * 2;
}
}
4 count doubled toList println;
-- List(0, 2, 4, 6)