Although definitions of the two vary, coroutines and continuations are closely related. Typically, first-class continuations are functions that execute from the given position in the program. As such, it is possible to invoke the same continuation many times – each time, the execution continues from the point that the continuation function represents.

Defined this way, first-class continuations are more expressive than standard coroutines. It is possible to implement coroutines using continuations, but it is not possible to express continuations using coroutines. The reason is that a continuation object can be resumed many times, whereas resuming a coroutine instance irreparably changes its state.

To make continuations and coroutines equally powerful abstractions, we need to add a straightforward extension to coroutines – namely, the snapshot operation on the coroutine instance. This part of the guide explains how to use capture coroutine snapshots.

Capturing a snapshot

Assume we have the following simple coroutine called values:

val values = coroutine { () =>
  yieldval(1)
  yieldval(2)
  yieldval(3)
}

As we learned in the previous sections, we can start a coroutine instance of values, and then resume it to yield a value:

val c = call(values())
assert(c.resume)
assert(c.value == 1)

Now, the coroutine instance c is suspended at the first yieldpoint. To obtain a snapshot of this state, i.e. duplicate the coroutine instance, we call the snapshot method:

val c2 = c.snapshot

Note that the snapshot method can only be called while the coroutine is suspended – but it is illegal to call snapshot on a coroutine that is currently executing. After this, we can continue invoking the coroutine instance operations on c, just like we did before.

assert(c.resume)
assert(c.value == 2)
assert(c.resume)
assert(c.value == 3)

However, after the coroutine instance c completes, we can continue calling the coroutine c2, which is still suspended on the first yieldpoint:

assert(c2.resume)
assert(c2.value == 2)
assert(c2.resume)
assert(c2.value == 3)

We can see that the coroutine instance c2 behaves in exactly the same way as the instance c did.

A coroutine instance snapshot operation duplicates a coroutine instance. Capturing a coroutine instance snapshot duplicates the state of the local variables on the instance stack, and its execution state (i.e. program counter). This does not duplicate any other (global) objects that the local variables are pointing to, and does not capture the state of the entire program runtime.

The complete snippet for this example is shown below.


Example use-case: backtracking testing suite

The previous example was simple, but it did not feel like a real use-case. In this section, we study how to implement a backtracking test suite, which enables tests that simultaneously execute different control paths in the test snippet. We will introduce special mock values, which, when used in an expression, execute the snippet from that point on with different values.

Concretely, we will be able to write tests like this:

if (mock()) {
  assert(2 * x == x + x)
} else {
  assert(x * x / x == x)
}

Above, when the coroutine reaches myMockCondition.get(), it will execute the remainder of the snippet twice – once with the value true, and once with the value false.

Before we start, we introduce several helper classes. The Cell is just a placeholder for a Boolean value:

class Cell {
  var value = false
}

The mock coroutine creates a new Cell object, yields it to the caller, and resumes by returning the value from the caller:

val mock: ~~~>[Cell, Boolean] = coroutine { () =>
  val cell = new Cell
  yieldval(cell)
  cell.value
}

The mock coroutine allows suspending the computation and taking a parameter from the caller, as we will soon see. The test method is where the magic happens. It takes a coroutine instance that yields Cell objects, and checks if it can be resumed. If resume returns false, the test is checked to see if it ended in an exceptional state. If resume returns true, the last yielded Cell is obtained, and its value is set to true. The test method is then run recursively with snapshot of the current coroutine. After it returns, the same procedure repeats with the value false in the Cell.

def test[R](c: Cell <~> R): Boolean = {
  if (c.resume) {
    val cell = c.value
    cell.value = true
    val res0 = test(c.snapshot)
    cell.value = false
    val res1 = test(c)
    res0 && res1
  } else c.hasResult
}

If we take a look at the mock coroutine again, we will see that it will first return true and then false. We can use mock in a test coroutine to enable the testing of different control paths:

val myAlgorithm = coroutine { (x: Int) =>
  if (mock()) {
    assert(2 * x == x + x)
  } else {
    assert(x * x / x == x)
  }
}

The myAlgorithm coroutine can now be invoked with different values:

assert(test(call(myAlgorithm(5))))
assert(!test(call(myAlgorithm(0))))

For each of the invocations, both branches of myAlgorithm will be executed. The second invocation will return false, because the second branch will throw an exception for x == 0. The complete example is shown below.


Summary

To summarize, we learned the following:

  • Every coroutine instance can be duplicated by calling its snapshot method. The so-obtained coroutine snapshot is a new coroutine instance with a fresh copy of the local variables, suspended at the same point as the original instance.
  • A coroutine snapshot does not include duplicating any other objects that the coroutine instance is referring to – the state of the runtime is not duplicated.
  • A coroutine may only be copied while it is suspended.