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.

Complex forms

Initial view

Initial view

Forms with multiple fields are supported, which lets form-handling functions return more than just a string.

def workflow(): NodeSeq @imp = {
  val (name, age) = getNameAndAge()
  val book = getBook()

  <html>
    <body>
      <h1>Greeter</h1>
      <div>Hello { name }, you are { age } years old and your favorite book is { book }!</div>
    </body>
  </html>
}

Stateful forms

Half-filled out

Half-filled out

Validation error

Validation error

Validation error

Validation error

Succesful submission

Succesful submission

Fields can be individually validated, and in case of an invalid form, only invalid fields need to be resubmitted. This works by storing the state of the workflow, including valid inputs, within the continuation itself.

def getNameAndAge(params: Map[String, String] = Map.empty): (String, String) @imp = {

  var inputs: Map[String, String] = Map.empty
  var errors: Map[String, String] = Map.empty

  var name: String = ""
  var age: String = ""

  validateName(params) match {
    case Left(fields) => inputs = inputs ++ fields.inputs; errors = errors ++ fields.errors
    case Right(_name)  => name = _name
  }

  validateAge(params) match {
    case Left(fields) => inputs = inputs ++ fields.inputs; errors = errors ++ fields.errors
    case Right(_age)  => age = _age
  }

  if (inputs.size > 0) getNameAndAge(params ++ prompt(form(inputs, errors)))
  else (name, age)
}

I like this approach because it eliminates the need to leak state by storing valid inputs in some mutable map somewhere outside the getNameAndAge function.

Session-based workflow

The state of the workflow, a continuation, is stored in an HTTP session variable so multiple users can access the workflow without stepping on each other's progress.

trait ImperativelyServlet
  extends ScalatraServlet
  with Imperatively[Map[String, String], NodeSeq] {

  def nextStep =
    if (session.contains("step")) session("step").asInstanceOf[Step]
    else (x: Map[String, String]) => imperatively

  def nextStep_=(next: Step) = session("step") = next

  get("/") { nextStep(params.toMap) }
}

Web-less workflow

The Imperatively trait no longer uses servlet-related code, and it generalizes on the workflow step input and output types, so it can be used as a general-purpose workflow engine.

trait Imperatively[A,B] {

  type Step = A => B

  type imp = cps[B]

  def nextStep: Step
  def nextStep_=(next: Step): Unit

  def prompt(b: B): A @imp =
    shift { k: (A => B) =>
      nextStep = k
      b
    }

  def imperatively: B = reset {
    val resp: B = workflow()
    nextStep = (x: A) => imperatively
    resp
  }

  def workflow(): B @imp
}

Complete example

Setup

Let's configure sbt with xsbt-web-plugin.

project/build.properties:

sbt.version=0.13.6

project/build.sbt:

addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "3.0.0")

build.sbt:

scalaVersion := "2.10.2"

addCompilerPlugin("org.scala-lang.plugins" % "continuations" % "2.10.2")

scalacOptions += "-P:continuations:enable"

libraryDependencies ++=
  Seq( "org.scalatra"      %% "scalatra"     % "2.2.0"
     , "javax.servlet"     %  "servlet-api"  % "2.5"
     , "com.earldouglas"   %% "imperatively" % "0.1.0-SNAPSHOT"
     )

enablePlugins(JettyPlugin)

src/main/webapp/WEB-INF/web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5">

  <servlet>
    <servlet-name>boring</servlet-name>
    <servlet-class>Boring</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>boring</servlet-name>
    <url-pattern>/boring</url-pattern>
  </servlet-mapping>

  <servlet>
    <servlet-name>greeter</servlet-name>
    <servlet-class>Greeter</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>greeter</servlet-name>
    <url-pattern>/greeter</url-pattern>
  </servlet-mapping>

  <servlet>
    <servlet-name>shopping-cart</servlet-name>
    <servlet-class>ShoppingCart</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>shopping-cart</servlet-name>
    <url-pattern>/shopping-cart</url-pattern>
  </servlet-mapping>

</web-app>

Fire it up

$ sbt jetty:start jetty:join
2012-01-04 20:15:17.726:INFO:oejs.Server:main: Started @5114ms

Boring

Boring

Boring

class Boring extends com.earldouglas.imperatively.ImperativelyServlet {

  import scala.xml.NodeSeq

  def workflow(): NodeSeq @imp = {
    <html>
      <body>
        <h1>Boring</h1>
        <div>That was not very interesting.</div>
      </body>
    </html>
  }
}

Greeter

Boring

Boring

class Greeter extends com.earldouglas.imperatively.ImperativelyServlet {

  import scala.xml.NodeSeq

  def workflow(): NodeSeq @imp = {
    val (name, age) = getNameAndAge()
    val book = getBook()

    <html>
      <body>
        <h1>Greeter</h1>
        <div>Hello { name }, you are { age } years old and your favorite book is { book }!</div>
      </body>
    </html>
  }

  def getNameAndAge(params: Map[String, String] = Map.empty): (String, String) @imp = {

    var inputs: Map[String, String] = Map.empty
    var errors: Map[String, String] = Map.empty

    var name: String = ""
    var age: String = ""

    validateName(params) match {
      case Left(fields) => inputs = inputs ++ fields.inputs; errors = errors ++ fields.errors
      case Right(_name)  => name = _name
    }

    validateAge(params) match {
      case Left(fields) => inputs = inputs ++ fields.inputs; errors = errors ++ fields.errors
      case Right(_age)  => age = _age
    }

    if (inputs.size > 0) getNameAndAge(params ++ prompt(form(inputs, errors)))
    else (name, age)
  }

  case class Fields(inputs: Map[String, String], errors: Map[String, String])

  def validateName(params: Map[String, String] = Map.empty): Either[Fields, String] =
    params.get("Your name") match {
      case None                          => Left(Fields(Map("Your name" -> ""), Map.empty))
      case Some(str) if str.trim.isEmpty => Left(Fields(Map("Your name" -> ""), Map("Your name" -> "You must provide your name.")))
      case Some(name)                    => Right(name.trim)
    }

  def validateAge(params: Map[String, String] = Map.empty): Either[Fields, String] =
    params.get("Your age") match {
      case None                                  => Left(Fields(Map("Your age" -> ""), Map.empty))
      case Some(str) if str.trim.isEmpty         => Left(Fields(Map("Your age" -> ""), Map("Your age" -> "You must provide your age.")))
      case Some(age) if age.trim.matches("\\d+") => Right(age.trim)
      case Some(str)                             => Left(Fields(Map("Your age" -> str), Map("Your age" -> "Your age must be a number.")))
    }

  def getBook(params: Map[String, String] = Map.empty): String @imp =
    params.get("Favorite book") match {
      case None                          => getBook(prompt(form(Map("Favorite book" -> ""), Map.empty)))
      case Some(str) if str.trim.isEmpty => getBook(prompt(form(Map("Favorite book" -> ""), Map("Favorite book" -> "You must provide the title."))))
      case Some(book)                    => book.trim
    }

  def form(inputs: Map[String, String], errors: Map[String, String]): NodeSeq =
    <html>
      <body>
        <h1>Greeter</h1>
        <form>
          {
            inputs.map { input =>
              <div style="clear:both;margin-bottom:10px">
                <span style="float:left;width:150px;text-align:right;margin-right:10px">{ input._1 }:</span>
                <span style="float:left;"><input name={ input._1 } value={ input._2 } />
              {
                errors.get(input._1) match {
                  case None      =>
                  case Some(msg) => <span style="margin:10px;color:red">{ msg }</span>
                }
              }
                </span>
              </div>
            }.toSeq
          }
          <div style="clear:both">
            <span style="float:left;margin-left:110px;margin-top:10px"><input type="submit" value="Submit" /></span>
          </div>
        </form>
      </body>
    </html>
}

Shopping cart

Shopping cart

Shopping cart

case class Item(name: String, price: Float)
case class ShippingInfo(name: String, address: String)
case class BillingInfo(name: String, address: String)
case class Order(items: List[Item], shippingInfo: ShippingInfo, billingInfo: BillingInfo)

sealed trait Confirmation
case object Confirm extends Confirmation
case object Cancel extends Confirmation

class ShoppingCart extends com.earldouglas.imperatively.ImperativelyServlet {

  import scala.util.continuations.cpsParam
  import scala.util.continuations.shiftUnit
  import scala.xml.NodeSeq
  import ShoppingCartViews._

  def workflow(): NodeSeq @imp = {
    val items            = getItems()
    val shippingInfo     = getShippingInfo()
    val billingInfo      = getBillingInfo()
    val unprocessedOrder = Order(items, shippingInfo, billingInfo)
    val confirmation     = getConfirmation(unprocessedOrder)

    if (confirmation) {
      val processedOrder = processOrder(unprocessedOrder)
      orderSummaryView(processedOrder)
    } else {
      orderCancellationView(unprocessedOrder)
    }
  }

  private val inventory: Map[String, Item] = Map("Gadget" -> Item("Gadget", 199), "Widget" -> Item("Widget", 99))

  def getItems(params: Map[String, String] = Map.empty, cart: List[Item] = Nil): List[Item] @imp = {

    var items: List[Item] = cart

    params.get("add").map(name => inventory.get(name).foreach(item => items = item :: items))
    params.get("del").map(name => inventory.get(name).foreach(item => items = items.take(items.lastIndexOf(item)) ++ items.drop(items.lastIndexOf(item) + 1)))

    params.get("checkout") match {
      case None    => getItems(prompt(inventoryView(items, inventory.values.toList)), items)
      case Some(_) => items
    }
  }

  def getShippingInfo(): ShippingInfo @imp = {
    val params = prompt(shippingInfoView)
    ShippingInfo(params.getOrElse("name", ""), params.getOrElse("address", ""))
  }

  def getBillingInfo(): BillingInfo @imp = {
    val params = prompt(billingInfoView)
    BillingInfo(params.getOrElse("name", ""), params.getOrElse("address", ""))
  }

  def getConfirmation(unprocessedOrder: Order): Boolean @imp = {
    val params = prompt(confirmationView(unprocessedOrder))
    params.get("confirmation") match {
      case Some("confirm") => shiftUnit(true)
      case Some("cancel")  => shiftUnit(false)
      case _               => getConfirmation(unprocessedOrder)
    }
  }

  def processOrder(unprocessedOrder: Order): Order = {
    // insert fancy order-processing logic
    val processedOrder = unprocessedOrder.copy()
    processedOrder
  }
}

object ShoppingCartViews {
  private def template(content: NodeSeq = Seq.empty): NodeSeq =
    <html>
      <head>
        <title>Shopping Cart Demo</title>
        <style type="text/css">
          {"""
            a    { text-decoration: none; color: blue; }
            span { margin: 10px; }
          """}
        </style>
      </head>
      <body>
        <a href="..">home</a>
        { content }
      </body>
    </html>

  def inventoryView(cart: List[Item], inventory: List[Item]): NodeSeq = {
    val content =
      <div>
        {
          if (cart != Nil) {
            <h2>Shopping cart</h2>
            <p><span><a href="?checkout">checkout</a></span></p>
            <table>
              <tr><th>Item</th><th>Price</th><th></th></tr>
              {
                cart.map { item =>
                  <tr><td>{ item.name }</td><td align="right">{ "$%1.2f" format item.price }</td><td><a href={ "?del=" + item.name }>[-]</a></td></tr>
                }
              }
            </table>
          }
        }
        <h2>Inventory</h2>
        <table>
          <tr><th>Item</th><th>Price</th><th></th></tr>
          {
            inventory.map { item =>
              <tr><td>{ item.name }</td><td align="right">{ "$%1.2f" format item.price }</td><td><a href={ "?add=" + item.name }>[+]</a></td></tr>
            }
          }
        </table>
      </div>
    template(content)
  }

  val shippingInfoView: NodeSeq = template {
    <h1>Shipping Info</h1>
    <form>
      <table>
        <tr><td align="right">Name: </td><td><input type="text" size="20" name="name" /></td></tr>
        <tr><td align="right">Address: </td><td><input type="text" size="20" name="address" /></td></tr>
        <tr><td colspan="2" align="right"><input type="submit" value="submit" /></td></tr>
      </table>
    </form>
  }

  val billingInfoView: NodeSeq = template {
    <h1>Billing Info</h1>
    <form>
      <table>
        <tr><td align="right">Name: </td><td><input type="text" size="20" name="name" /></td></tr>
        <tr><td align="right">Address: </td><td><input type="text" size="20" name="address" /></td></tr>
        <tr><td colspan="2" align="right"><input type="submit" value="submit" /></td></tr>
      </table>
    </form>
  }

  def confirmationView(order: Order): NodeSeq = template {
    <h1>Confirmation</h1>
    <p>
      <span><a href="?confirmation=confirm">confirm</a></span>
      <span><a href="?confirmation=cancel">cancel</a></span>
    </p>
    <div>{ orderView(order) }</div>
  }

  def orderView(order: Order): NodeSeq = {
    <h2>Items</h2>
    <table>
      <tr><th>Item</th><th>Price</th></tr>
      { order.items.map { item => <tr><td>{ item.name }</td><td align="right">{ "$%1.2f" format item.price }</td></tr> } }
    </table>
    <h2>Shipping Info</h2>
    <table>
      <tr><td align="right">Name: </td><td>{ order.shippingInfo.name }</td></tr>
      <tr><td align="right">Address: </td><td>{ order.shippingInfo.address }</td></tr>
    </table>
    <h2>Billing Info</h2>
    <table>
      <tr><td align="right">Name: </td><td>{ order.billingInfo.name }</td></tr>
      <tr><td align="right">Address: </td><td>{ order.billingInfo.address }</td></tr>
    </table>
  }

  def orderSummaryView(order: Order): NodeSeq = template {
    <h1>Order Summary</h1>
    <p>Thank you for your order!</p>
    <div>{ orderView(order) }</div>
  }

  def orderCancellationView(order: Order): NodeSeq = template {
    <h1>Order Cancelled</h1>
    <p>Come back soon!</p>
    <div>{ orderView(order) }</div>
  }
}