Coroutine
A coroutine is a function that can stop part-way — yield a value — and
later be resumed exactly where it stopped, with its locals and its whole
call stack intact. Coroutine<T> is the handle you resume; the body is handed
a Yielder<T>, the one thing that can yield it. The language guide’s
Coroutines section explains the model; this is the
reference.
-- A generator: the declared return type tells the body what it yields.fn countdown(start: integer) -> Coroutine<integer> return Coroutine.create(fn(co) for i = start, 1, -1 do co.yield(i) end end)end
for n in countdown(3) do println(n) -- 3, then 2, then 1endBoth types are generic. T is learned from wherever the coroutine goes — a
typed local, a declared return type, a parameter — or from an annotation on
the body’s yielder, fn(co: Yielder<string>). Coroutines are stackful:
pass the yielder to any function and that function can yield too.
Creating
Section titled “Creating”Coroutine.create<T>(body: fn(Yielder<T>) -> nil) -> Coroutine<T>
A new coroutine that has not started. Nothing in body runs until the first
resume, which calls it with the coroutine’s yielder.
local ran: boolean = falselocal gen: Coroutine<string> = Coroutine.create(fn(co) ran = true co.yield("first")end)
println(ran) -- false: creating runs nothingprintln(gen.resume() ?? "") -- firstprintln(ran) -- trueCoroutine methods
Section titled “Coroutine methods”gen.resume() -> T?
Run the coroutine until it next yields, and answer the value it yielded. When
the body finishes — or has already — it answers nil, and so it does when
the body fails: a runtime error or an uncaught throw inside a coroutine
does not end the program. The coroutine is dead afterwards and
error() says what went wrong.
Resuming a coroutine that is already running — from inside itself, or one waiting on a coroutine it resumed — is a runtime error.
local gen: Coroutine<integer> = Coroutine.create(fn(co) co.yield(10) co.yield(20)end)
println(gen.resume() ?? 0) -- 10println(gen.resume() ?? 0) -- 20println(gen.resume() == nil) -- true: finishedprintln(gen.resume() == nil) -- true: still finishedgen.status() -> string
Where the coroutine is in its life:
| Status | Meaning |
|---|---|
"suspended" |
Created and not yet started, or stopped at a yield |
"running" |
Its code is running now — only seen from inside it |
"normal" |
It resumed another coroutine and is waiting on it |
"dead" |
Its body finished, or failed |
local gen: Coroutine<integer> = Coroutine.create(fn(co) co.yield(1)end)
println(gen.status()) -- suspendedgen.resume()println(gen.status()) -- suspended, at the yieldgen.resume()println(gen.status()) -- deadgen.isDone() -> boolean
true once the coroutine is dead — finished or failed. Shorthand for
gen.status() == "dead", and the natural test for a loop that drives several
coroutines at once.
local gen: Coroutine<integer> = Coroutine.create(fn(co) co.yield(1)end)
while not gen.isDone() do gen.resume()endprintln(gen.status()) -- deadgen.error() -> string?
Why the coroutine failed, or nil if it has not. For an uncaught throw it
is the thrown value; for any other failure, the runtime error’s message.
local gen: Coroutine<integer> = Coroutine.create(fn(co) co.yield(1) throw "out of data"end)
gen.resume() -- 1println(gen.resume() == nil) -- true: it failedprintln(gen.status()) -- deadprintln(gen.error() ?? "") -- out of datagen.iter() -> fn() -> T?
A step closure that resumes the coroutine once per call — what
for v in gen do uses, so you rarely call it yourself. A coroutine is an
Iterable<T>, and the loop variable is a T.
Unlike resume, iterating raises if the coroutine fails part-way: a loop
has no return value to report a failure in, and ending quietly would make a
generator that crashed look like one that finished. Breaking out of the loop
leaves the coroutine suspended where it was.
local gen: Coroutine<integer> = Coroutine.create(fn(co) for i = 1, 5 do co.yield(i * i) endend)
for sq in gen do if sq > 10 then break -- gen stays suspended end println(sq) -- 1, 4, 9endprintln(gen.resume() ?? 0) -- 25: picks up after the breakYielder methods
Section titled “Yielder methods”co.yield(value: T) -> nil
Suspend the coroutine this yielder belongs to and hand value to whoever
resumed it. The call returns when the coroutine is next resumed.
Only the coroutine’s own code can yield it. Two places cannot, and saying so is a runtime error:
- inside a callback a built-in makes — a
Table.sortcomparator, an operator overload, atoString. The built-in’s own call is in the way and cannot be suspended. The coroutine fails, anderror()says why; - outside the coroutine — a yielder that escaped its body and is used while some other coroutine, or none, is running.
-- A yielder passes down like any value, so any function can yield.fn walk(co: Yielder<integer>, depth: integer) if depth == 0 then return end co.yield(depth) walk(co, depth - 1)end
local gen: Coroutine<integer> = Coroutine.create(fn(co) walk(co, 3)end)
for d in gen do println(d) -- 3, 2, 1endDynamic coroutines
Section titled “Dynamic coroutines”The typed contract is fn(Yielder<T>) -> nil: values go out through
yield, and resume takes nothing. Lua’s coroutines also pass values in —
resume(x) becomes the result of the yield it resumes — and give the body’s
return value to the last resume. Saule’s runtime does all of that; a body
held as any is how to reach it, and everything about it is checked at run
time instead.
local body: any = fn(co: any, start: integer) -> string local got: any = co.yield(start) return "got " .. tostring(got)end
local gen: any = Coroutine.create(body)println(gen.resume(10)) -- 10: the first resume's arguments reach the bodyprintln(gen.resume("back")) -- got back