Tzopilotl
Docs
GitHub

Tzopilotl by Example

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";
}
Note: 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)