Coroutines - IS4Code/Sona GitHub Wiki
Coroutines are objects that can be created using a computation block or other means and represent a resumable piece of execution to facilitate in cooperative multitasking, similarly to Lua coroutines.
A basic coroutine is an instance of Sona.Runtime.Coroutines.ICoroutine<TInput, TElement, TResult>, offering several methods and properties to control it. A coroutine may go through several states throughout its lifetime: suspended (when it yields a value of type TElement and expects TInput to continue), faulted (when the execution ended in an exception), or finished (when the coroutine ended on a return with a TResult).
Coroutines might be created by implementing one of their interfaces, by using the functions of the Coroutine package, or using the coroutine workflow, which is the preferred option for coroutines running a code-based state machine:
function yieldValues()
with coroutine
yield 1
yield 2
return "done"
endThis creates an instance of ICoroutine<unit, int, string> ‒ using the yield statement indicates there is no input (the type is unit, because yield does not return a value). It is however possible to use Coroutine.yield and follow to retrieve the input:
function stringifyValues(first)
with coroutine
var input = first
while true do
input = follow Coroutine.yield(string input)
end
endThis function takes some initial input and creates an instance of ICoroutine<, string, unit> (the input type is inferred) that converts all input values into strings and yield them.
Coroutines created this way are always eager ‒ when the function is called, the code before with is immediately followed by the code after with, until the first moment a value is returned, yielded, or an exception is thrown.
The coroutine workflow treats coroutines as monadic on TResult, meaning an operation returning TResult can be lifted to an operation returning ICoroutine<,, TResult>. This implies the same use of coroutines as in Lua ‒ what a function implemented using a coroutine yields is generally not of use to the caller that uses follow to bind to its result:
function operation()
with coroutine
yield "Starting operation"
// ...
yield "Operation done"
return 1
end
function main()
with coroutine
let result = follow operation()
process(result + 1)
endWhoever inspects the coroutine created by main() may retrieve the individual yielded values and decide whether to resume or not, but within the coroutine, the flow is not affected by them.
For this reason, yield.. is equivalent to plain follow (expects a unit-returning coroutine).
A created coroutine can be mechanically operated using several of its methods, and its State property can be observed to direct behaviour:
function cor()
with coroutine
for i in 1 .. 10 do
yield i
end
return "result"
end
let c = cor()
switch c.State
case Paused do
echo $"Paused; resuming..."
c.Resume()
continue c.State
case Yielded(element) do
echo $"Yielded {element}; resuming..."
c.Resume()
continue c.State
case Finished(result) do
echo $"Result: {result}"
case Faulted(error) do
echo $"Faulted: {error}"
endThe state of the coroutine is also deconstructed into properties of the actual coroutine object (Status, Current, Result, Exception) but these are useful only in .NET languages that cannot benefit from exhaustiveness checks on switch c.State.
The extra Paused state appears only in special cases and indicates a suspended coroutine without a given result. This state may also be triggered manually via follow Coroutine.pause().
The supported operations are:
- Resuming without any input (
Resume()): this works ifTInputisunitor an option type, or if the coroutine is in a special state where input is not expected (only in thePausedstate). - Resuming with an input (
Resume(input)): this is the usual way of resumption that should be used to resume from aYieldedstate. This operation is also allowed if the coroutine is in a state that does not require an input butTInputisunitor an option type andinputis the default value. - Resuming with an exception (
ResumeThrow(error)): this works in both suspended states to throw an exception from the point of suspension, which can then be observed within the coroutine.
All these methods throw an exception if the coroutine is not in a state that permits the operation. For exception-less code, Try overloads are also available, which return whether the operation succeeded.