When the interpreter is given a Function to execute, it first begins by
reading the arguments given. Once the arguments are prepared, it begins at the
top of the Function and flows down. That flow continues until there is a
return, or an uncaught exception occurs.
Lily's prelude includes a coroutine module with a Coroutine class. Lily's
Coroutines wrap over a Function, allowing it to be suspended for later.
Coroutine does not have a constructor or special creation syntax like List.
Instead, a Coroutine is created using either Coroutine.build or
Coroutine.build_with_value.
This is a basic Coroutine that yields five numbers.
import (Coroutine) coroutine
define one_to_five(co: Coroutine[Integer, Unit]): Integer
{
for i in 1...3: {
co.yield(i)
}
co.yield(4)
return 5
}
var co = Coroutine.build(one_to_five)
print(co.resume()) # Success(1)
print(co.resume()) # Success(2)
print(co.resume()) # Success(3)
print(co.resume()) # Success(4)
print(co.resume()) # Success(5)
The Coroutine.build method requires the Function it wraps over to take a
Coroutine as the first argument. The type is given as
Coroutine[Integer, Unit] because the Coroutine yields (or returns)
Integer, but does not take any value.
Since Lily is statically-typed, a Coroutine that yields a value must also
return that same type. Since the above Function yields Integer, it must
return Integer.
What about sending an argument to a Coroutine?
import (Coroutine) coroutine
define yield_total(co: Coroutine[Integer, Integer]): Integer
{
var total = 0
for i in 1...3: {
total += co.receive()
co.yield(total)
}
return total + co.receive()
}
var co = Coroutine.build(yield_total)
print(co.resume_with(1)) # Success(1)
print(co.resume_with(2)) # Success(3)
print(co.resume_with(3)) # Success(6)
print(co.resume_with(4)) # Success(10)
The Function that a Coroutine wraps over cannot itself have arguments.
Instead, Coroutine.build_with_value allows sending a single value over. There
are no restrictions on the types being sent or received:
import (Coroutine) coroutine
define number_list(co: Coroutine[List[Integer], String]): List[Integer]
{
var total = 0
while 1: {
var str = co.receive()
var l = str.split(",")
.map(|m| m.trim().parse_i())
.select(Option.is_some)
.map(Option.unwrap)
co.yield(l)
}
return []
}
var co = Coroutine.build(number_list)
print(co.resume_with("1,22,333")) # Success([1, 22, 333])
print(co.resume_with("abc,123,def,4,56")) # Success([123, 4, 56])
Every Coroutine exists in one of the following states.
Done: The Coroutine's Function has returned. (Coroutine.is_done).
Failed: An exception was raised in the Coroutine
(Coroutine.is_failed).
Running: The Coroutine is currently being executed. If a Coroutine
invokes another, both will have this status (Coroutine.is_running).
Waiting: This Coroutine has not returned or failed. This is the initial
state of a Coroutine. If a Coroutine does not fail or finish, it will be
this state after running (Coroutine.is_waiting).
Another way to determine the status of a Coroutine is through calling
Coroutine.status like so:
import (Coroutine) coroutine
define example(co: Coroutine[Unit, Unit]) {}
print(Coroutine.build(example).status()) # CoStatus.Waiting
Due to how Coroutine is implemented, it is not possible to yield while inside
of a foreign function. Attempting to do so will result in an exception in the
Coroutine, and no value passing to the caller. In particular, Hash and
List iteration methods are foreign functions, so it is not possible to yield
inside of them.
Coroutine.build and Coroutine.build_with_value can technically raise
RuntimeError if the Function given is not a native one. In practice, that is
only possible with code like Coroutine.build(Coroutine.resume), which does not
make sense anyway.
The value given Coroutine.resume_with is transferred to the Coroutine. If
the Coroutine can be resumed, the prior resumption value is removed (and
deref'd, if reference counted).
Due to how Coroutine is implemented, it is not possible to use
Coroutine.yield inside of a foreign function. Iteration methods (such as
List.each) are foreign functions. Attempting to do so will raise
RuntimeError within the Coroutine.
Coroutine.resume and Coroutine.resume_with both return a Result, with the
Failure as an Exception.
If resumption is given a Coroutine that is not waiting, it will return a
Failure of CoError. That CoError will contain traceback of the caller
(and not the Coroutine).
All other exceptions from resumption will have a Failure of the exception
raised, with the traceback of the Coroutine.