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.

Creation

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

Status

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

Exceptions

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.

Technical details

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.