Delimited Continuations

July 7, 2012

Consider the following program:

def run(): Unit = {
  println("What is your name?")
  val name = readLine()
  println(s"Hello, ${name}!")
}

We've all written this kind of program when we were first learning to code. It turns out to be a good place to start learning about continuations!

What's really going on with this program?

  1. A message is printed to the console
  2. A line is read from the console
  3. Another message is printed to the console

When the program reads a line from the console, something interesting has happened. The flow of the program has been interrupted, and it cannot proceed until the user has typed a line of text. The rest of the program is now a function that takes a String and prints a message to the console, and is invoked only when the user has provided input.

The "rest of the program" is a continuation, and looks like this:

val k = { name: String => println(s"Hello, ${name}!") }

The run method can be rearranged to invoke the continuation once the user has typed a line of text.

def run1(): Unit = {
  println("What is your name?")
  k(readLine())
}

Let's rewrite run1 using Scala's delimited continuations primitives, shift and reset.

import scala.util.continuations.cpsParam
import scala.util.continuations.reset
import scala.util.continuations.shift
def run2(): Unit = reset {
  println("What is your name?")
  val name = shift { k: (String => Unit) =>
    val name = readLine()
    k(name)
  }
  println(s"Hello, ${name}!")
}

The flow of the program has not changed. Printing the second message to the screen becomes a continuation of type String => Unit, which is passed into the shift block as the function k, to be invoked with the user's input.

Now we can start to do some neat things with the continuation. There's no reason that k has to be invoked from within the shift block. Maybe we don't want to wait for the user to input a line of text, or maybe we don't even know when or from where the input will come. In either case, we can put the continuation off to the side, so we can invoke it by some other means.

var c3: String => Unit = _

def run3(): Unit = reset {
  println("What is your name?")
  val name = shift { k: (String => Unit) =>
    c3 = k
  }
  println(s"Hello, ${name}!")
}

Invoking run3 will stick the continuation (which in this case is everything after the shift block -- a function that takes a String and prints a message to the console) into the variable c, which we can invoke later (eg from the REPL, another function, etc.).

The shift block is somewhat tricky to read, and interrupts the visual interpretation of what the run3 method is intended to do. To clean things up, it can be refactored into a more descriptive method, prompt.

var c4: String => Unit = _

def prompt4() = shift { k: (String => Unit) => c4 = k }

def run4(): Unit = reset {
  println("What is your name?  ** call c4(<your name>) to continue **")
  val name = prompt4()
  println(s"Hello, ${name}!")
}

It is easy to read the run4 method and have a general idea of what it does, accepting that we're not specific on how prompt prompts for user input.

The imperative-looking prompt method can be used to cleanly embed many shift blocks in the run4 method, so we can have multiple-step asynchronous user workflows.

var c5: String => Unit = _

def prompt5(): String @cpsParam[Unit,Unit] =
  shift { k: (String => Unit) => c5 = k }

def run5(): Unit = reset {
  println("What is your name?  ** call c5(<your name>) to continue **")
  val name = prompt5()
  println(s"Hello, ${name}!")

  println("How old are you?  ** call c5(<your age>) to continue **")
  val age = prompt5()
  println(s"You are ${age} years old, ${name}!")

  println("Where do you live?  ** call c5(<your town>) to continue **")
  val town = prompt5()
  println(s"You are ${age} years old and live in ${town}, ${name}!")
}

Running this in the REPL looks like this:

scala> run5()
What is your name?  ** call c5(<your name>) to continue **

scala> c5("James")
Hello, James!
How old are you?  ** call c5(<your age>) to continue **

scala> c5("29")
You are 29 years old, James!
Where do you live?  ** call c5(<your town>) to continue **

scala> c5("Palo Alto")
You are 29 years old and live in Palo Alto, James!

scala>

sbt configuration

See the compiler plugin support section of the sbt documentation for the latest configuration information.

For sbt 1.0 and Scala 2.12, use the following:

/***
scalaVersion := "2.12.2"

addCompilerPlugin( "org.scala-lang.plugins"
                 % "scala-continuations-plugin_2.12.2"
                 % "1.0.3"
                 )

libraryDependencies +=
  "org.scala-lang.plugins" %% "scala-continuations-library" % "1.0.3"

scalacOptions += "-P:continuations:enable"
*/

Demo

run5()
c5("James")
c5("29")
c5("Palo Alto")

This file is literate Scala, and can be run using Codedown:

$ curl https://earldouglas.com/funtinuations.md |
  codedown scala > script.scala
$ sbt -Dsbt.main.class=sbt.ScriptMain script.scala
What is your name?  ** call c5(<your name>) to continue **
Hello, James!
How old are you?  ** call c5(<your age>) to continue **
You are 29 years old, James!
Where do you live?  ** call c5(<your town>) to continue **
You are 29 years old and live in Palo Alto, James!

References