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?
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) =>
= k
c3 }
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] =
{ k: (String => Unit) => c5 = k }
shift
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>
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:
/***
:= "2.12.2"
scalaVersion
( "org.scala-lang.plugins"
addCompilerPlugin% "scala-continuations-plugin_2.12.2"
% "1.0.3"
)
+=
libraryDependencies "org.scala-lang.plugins" %% "scala-continuations-library" % "1.0.3"
+= "-P:continuations:enable"
scalacOptions */
run5()
c5("James")
c5("29")
c5("Palo Alto")
This file is literate Scala, and can be run using Codedown:
$ curl https://earldouglas.com/posts/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!