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).

Creating coroutines

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"
end

This 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
end

This 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.

Combining coroutines

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)
end

Whoever 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).

Running coroutines

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}"
end

The 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 if TInput is unit or an option type, or if the coroutine is in a special state where input is not expected (only in the Paused state).
  • Resuming with an input (Resume(input)): this is the usual way of resumption that should be used to resume from a Yielded state. This operation is also allowed if the coroutine is in a state that does not require an input but TInput is unit or an option type and input is 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.

⚠️ **GitHub.com Fallback** ⚠️