Responder - continuations in Scala

This writeup assumes you have some familiarity with continuations. The Responder class and the examples are by Martin Odersky, althought the continuation monad in Scala has been known for some time before. These comments by Burak Emir (but feel free to add/change).

For easy reference, here is the core code from the scala.Responder class and singleton object

object Responder {
 
  def constant[A](x: A) = new Responder[A] {
    def respond(k: A => Unit) = k(x)
  }
 
  def exec[A](x: => Unit): Boolean = { x; true }
 
  def run[A](r: Responder[A]): Option[A] = {
    var result: Option[A] = None
    r.foreach(x => result = Some(x))
    result
  }
 
  def loop[A](r: Responder[Unit]): Responder[Nothing] = 
    for (_ <- r; val y <- loop(r)) yield y
 
  def loopWhile[A](cond: => Boolean)(r: Responder[Unit]): Responder[Unit] = 
    if (cond) for (_ <- r; val y <- loopWhile(cond)(r)) yield y
    else constant(())
}
 
abstract class Responder[+A] {
 
  def respond(k: A => Unit): Unit
 
  def foreach(k: A => Unit) { respond(k) }
 
  def map[B](f: A => B) = new Responder[B] {
    def respond(k: B => Unit) {
      Responder.this.respond(x => k(f(x)))
    }
  }
 
  def flatMap[B](f: A => Responder[B]) = new Responder[B] {
    def respond(k: B => Unit) {
      Responder.this.respond(x => f(x).respond(k))
    }
  }
 
  def filter(p: A => Boolean) = new Responder[A] {
    def respond(k: A => Unit) {
      Responder.this.respond(x => if (p(x)) k(x) else ())
    }
  }
}

Some explanations for object Responder:

  • A constant responder always responds with the same value x,
  • exec looks profoundly useless but is helpful when adding arbitrary code in for-comprehensions,
  • running a responder may or may not produce a result, because of filtering,
  • loop and loopWhile provide repeated execution of a responder.

Some explanations for class Responder:

  • a Responder[A] is something like a computation that produces a value of type A,
  • every Responder[A] has to respond by sending that value to continuation k,
  • foreach simply delegates to respond since there is only one value being produced,
  • mapping a responder means mapping that value to something else,
  • flatMap (or “bind”) is the prime means of composing Responders,
  • filtering means checking some condition on the value. if false, subsequent continuations are ignored.

Some explanations on flatMap and for-comprehensions

  • since we use the names foreach, map, flatMap, and filter, we can construct programs from Responders by putting everything in for-comprehensions (see the spec if unsure about for-comprehensions). That means writing several lines of “val x ← rexp” where rexp is of type Responder[T]. However, one might just execute some imperative code instead of wrapping everything in responders, and that’s what exec is for. The reason of exec(code) returning a boolean is that it looks like a filter.

Simple Example: a Console Dialogue with a "Back" command

It’s probably easiest to understand how responders relate to continuations by solving an IO-problem in Scala: a dialogue, on a terminal, with the possible to go back and correct answers. For this, we’ll make use of a Reader class and a Reader object.

The Reader object will have a field containing a list of pairs (r,k) of a reader and a continuation. The idea is after each succesful input, it will “dump its stack”. This is not really practical if many of these dialogues are going to be used in your program, but it’s only a toy example and writing a reset routine is left as an exercise.

What goes in object Reader? A way to ask questions to the user. It constructs an instance of class Reader, which is a Responder that (quite lazily) gets the user input when presented with the remaining program (the continuation). If the users says “back”, it just grabs the last reader from the list, and then passes the continuation to that one.

object Readers {
  private var readers: List[Pair[Reader, String => unit]] = List()
 
  def ask(prompt: String) = new Reader(prompt)
 
  class Reader(prompt: String) extends Responder[String] {
 
    def respond(k: String => unit): unit = {
      print("["+(readers.length+1)+"]"+prompt+"> "); 
      val line = readLine;
      if (line == "back") {
        val Pair(rdr, k) = readers.head;
        readers = readers.tail
        rdr.respond(k)
      } else {
        readers = Pair(this, k) :: readers
        k(line)
      }
    }
  }
}

Now we can use our Readers to implement a dialogue that allows to go back. Keeping it no simpler than possible, the dialogue is asking for two numbers to be added, the first of which needs to be positive.

import Responder._
import Readers._
 
object Test1 extends Application {
  run {
    for { 
      val line1 <- ask("first number")
      val f = Integer.parseInt(line1)
      f > 0
      exec(println("first number parsed!"))
      val s <- ask("second number") map Integer.parseInt
    } yield f + s
  } match {
    case Some(x) => println("the sum is "+x)
    case None => println("(aborted)")
  }
}

Another Example: an endless dialogue (still with a "Back" command)

import Responder._
import Readers._
import Console._
 
object Test2 extends Application {
  def sumup(x: int): Responder[int] = 
    for {
      val line <- ask("enter a number");
      val rest <- if (line == "") constant(x) 
                  else if (line == "?") { println("sum so far: " + x); sumup(x) }
                  else sumup(x + Integer.parseInt(line))
    } yield rest
  println("sum = " + run(sumup(0)).get)
}
 
libs/responder.txt · Last modified: 2008/01/23 00:20 by 167.107.191.217
 
Recent changes RSS feed Valid XHTML 1.0 Driven by DokuWiki