Read the follow-up to this article: Continuation-Based Web Workflows, Part Two
Get the source code for this article: https://github.com/JamesEarlDouglas/imperatively
One of the neat uses of continuations is to represent blocking, asynchronous code as imperative-style functions. I put together a little library to help write Web-based workflows, which involve multiple round-trip steps between the client and server, with the user providing input along the way.
Consider the following:
def workflow(): NodeSeq @imp = {
val name = getName()
val age = getAge()
<html>
<body>
Hello {name}, you are {age} years old!
</body>
</html>
}
The workflow() function returns a Scala XML literal which greets a user and displays their age. There are two calls to other functions, getName() and getAge(), each of which need to prompt the user for input. It looks nice and imperative, but it actually uses continuations to create at least three separate HTTP responses to which the user must react.
def getName(input: Option[String] = None): String @imp =
input match {
case None => getName(prompt(form("Name")))
case Some(name) => name
}
def getAge(input: Option[String] = None): String @imp =
input match {
case None => getAge(prompt(form("Age")))
case Some(age) if age.matches("\\d+") => age
case _ => getAge(prompt(form("Age", Some("Your age must be a number."))))
}
def form(label: String, error: Option[String] = None) =
<html>
<body>
<form>
<p>{label}: <input name="input" /></p>
{
error match {
case None =>
case Some(msg) => <p style="color:red">{msg}</p>
}
}
</form>
</body>
</html>
The getName() and getAge() functions each prompt the user for an input value using the form method to generate the HTML. In the case of getAge(), some simple validation ensures that the user provides a number.
The continuation fun is hidden away in the Imperatively trait.
trait Imperatively extends ScalatraServlet {
type imp = cps [NodeSeq]
var step: (Option[String] => NodeSeq) = (x: Option[String]) => imperatively
def prompt(html: NodeSeq): Option[String] @imp =
shift { k: (Option[String] => NodeSeq) =>
step = k
html
}
def imperatively: NodeSeq = reset {
val resp: NodeSeq = workflow()
step = (x: Option[String]) => imperatively
resp
}
def workflow(): NodeSeq @imp
get("/") { step(params.get("input")) }
}
Extending this trait, a developer needs only to implement the workflow() method, calling prompt as needed to generate an additional HTTP response/request to collect user input.