In my previous post I showed a generic generator class built on top of my coroutines library. I commented in that post that I was taking the expedient approach of using my existing library, but that it would be possible to package all of the functionality into the
Generator class.
I soon realized that it would in fact be relatively easy to do that packaging, and that the resulting relatively simple class would probably make a good vehicle for demonstrating the usefulness of Scala's delimited continuations. Thus I give you the
StandaloneGenerator class:
package net.jimmc.scoroutine
import scala.collection.Iterator
import scala.util.continuations._
class StandaloneGenerator[T] extends Iterator[T] {
private var nextValue:Option[T] = None
private var nextStep:Option[Unit=>Unit] = None
/** Subclass calls this method to generate values.
* @param body The code for your generator.
*/
protected def generate(body: => Unit @suspendable) {
reset {
suspend
body
}
}
/** Yield the next generated value.
* Call this code from your generator to deliver the next value.
*/
protected def yld(x:T):Unit @suspendable = {
nextValue = Some(x)
suspend
}
/** Retrieve the next generated value.
* Call this from your main code.
*/
def next:T = {
step
nextValue match {
case None => throw new NoSuchElementException("next on empty generator")
//make it similar to the equivalent Iterator exception
case Some(x) => nextValue = None; x
}
}
/** True if there is another value to retrieve.
* Call this from your main code.
*/
def hasNext:Boolean = {
step
nextValue.isDefined
}
/** Save our continuation to resume later. */
private def suspend:Unit @suspendable = {
shift { k:(Unit=>Unit) =>
nextStep = Some(k)
}
}
/** If we have a next step but we don't have a value queued up,
* run the generator until it calls yld or completes. */
private def step = {
if (nextValue.isEmpty) nextStep foreach { nextStep = None; _() }
}
}
The StandaloneGenerator class is a plug-compatible replacement
for the Generator class described in my previous post,
as long as the derived generator does not use fancy scheduling as in the
Primes Generator example.
So, for example, you could take the
Integers Generator example from that post,
replace the two occurences of Generator with
StandaloneGenerator, and everything would work the same way.
We have two variables:
nextValue is our one-element queue where we store the
next value to be returned by the generator,
and nextStep is our "scheduler queue"
where we store our continuation each
time we suspend the generator to return a value.
Both of these variables are of type Option so that we can
tell when they hold a value.
Control is managed by the two functions
suspend
and step.
The suspend method has a shift block that
could not be much simpler: all it does is store the passed-in
continuation in the nextStep variable.
Since the body of a shift block is always the last thing
executed within the scope of the CPS code (delimited either by the
enclosing reset block or an explicitly invoked continuation),
the fact that suspend does not execute its continuation means
that after suspend does its thing,
control returns to the point just past the enclosing reset,
or to the next statement after the explicit continuation call.
I considered calling the
step method resume
instead, to make it clear that it was the complement to suspend,
but from the point of view of the code calling step,
by the time control returns to the point after the call to step,
the generator code has already been suspended again:
it has completed running one step, which is from one yld
call to the next.
The
step function executes our continuation
if we have one, but only
if we don't already have a value in nextValue.
Using foreach on an Option is a neat way
to execute a block of code only if the Option has a value
(i.e. is not None).
In this case, since the contents of nextStep (if it has any)
is a function, the placeholder variable _ gets set to
the continuation and
the code fragment _() is what actually
executes the continuation.
Here are two other ways this function could be implemented that
do the same thing:
private def step1 = {
if (nextValue.isEmpty) nextStep match {
case None => //do nothing
case Some(p) => nextStep = None; p()
}
}
private def step2 = {
if (nextValue.isEmpty && nextStep.isDefined) {
val p = nextStep.get; nextStep = None; p()
}
}
Let's walk through this and see how it works
(assuming the generator generates at least one value):
- The main code instantiates a generator, which passes its generator body
to the
generatefunction, which callssuspend, which saves that body innextStepwithout executing it. - The main code calls
hasNext, which callsstep, which runs the continuation. This starts the generator body code running, which runs until it callsyldwith the first value. That first value is stored innextValueand the generator code is suspended, with the continuation stored innextStep. Since we now have a value stored innextValue,hasNextreturnstrue. - The main code calls
next. Since we have a value innextValue, the call tostepdoes nothing (it is only there in case the caller callsnextwithout first callinghasNext). The value innextValueis returned, and that variable is cleared (set toNone). - Each time the main code calls
hasNextand it callsstep, the continuation stored innextStepis executed, the generator code callsyldwith the next value, that value is stored intonextValue, the continuation is stored innextStep, and the generator is suspended, andhasNextreturnstrue. - Eventually (for a finite generator) there is a call to
stepwhere the generator finishes execution without callingyld. When this happens, no values are stored in eithernextValueornextStep. Since there is no value innextValue,hasNextreturns false. Since there is also no value innextStep, further calls tostepdo nothing, andhasNextwill continue to returnfalse.
reset and shift operators
that you will find.
Two variables, six functions each not more than four lines of body code,
16 lines of function body code in total,
35 lines of code altogether.
3 comments:
How about this problem http://stackoverflow.com/questions/3797699/block-to-stream-convertion ?
I tried this but it doesn't work:
def data(block: Int => Unit) { for (i <- 0 to 10) block(i) }
class IntGen extends StandaloneGenerator[Int] {
generate {
var x = 1
data { x =>
yld(x)
}
}
}
Dawid: Your example can be made to work by fixing two problems:
1) Annotations. Since the block you are passing to your data function gets suspended by a call to yld, you must mark it as suspendable. Now, since your data function is calling a suspendable function, it too must be marked as suspendable. Thus you must change the signature of your data function to
def data(block: Int => Unit @suspendable): Unit @suspendable = ...
2) You can not currently use a "for" loop with a body that is suspendable. Instead, you can use a "while" loop as in Integer Generator in my previous post. In general you will need to be aware of the CPS restrictions that apply to any code that is to be suspended.
Once you have your generator working using the above StandaloneGenerator, you can easily convert it from an Interator to a Stream by calling toStream on it.
Post a Comment