Skip to content
This repository

Imperative Web workflows via delimited continuations

branch: master

Fetching latest commit…

Octocat-spinner-32-eaf2f5

Cannot retrieve the latest commit at this time

Octocat-spinner-32 readme
Octocat-spinner-32 sbt
Octocat-spinner-32 src
Octocat-spinner-32 .gitignore
Octocat-spinner-32 README.md
Octocat-spinner-32 build.sbt
README.md

Build Status

Imperatively is a little library for writing workflows with discrete, asynchronous steps in an imperative style. Ostensibly this makes code easier to read and maintain, but mostly it's a fun way to play with delimited continuations.

Web applications commonly have workflows which involve multiple round-trip request/response cycles to complete. Along the way, some state must be kept around to recall a user's progression through the workflow. Using delimited continuations, such a workflow can be written as a single function with asynchronous calls to collect user input.

Imagine a workflow with three steps: two separate HTML forms to gather input, and a final HTML view. With Imperatively, this might look something like the following.

def welcome(): NodeSeq = {
  val name = prompt(<input name="Name" />)
  val age  = prompt(<input name="Age" />)
  <h1>Hello { name }, you are { age } years old!</h1>
}

Demo

Check out https://github.com/JamesEarlDouglas/imperatively-demos

Continuation-Based Web Workflows

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

Greeter screenshot 1

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

Greeter screenshot 2Greeter screenshot 3Greeter screenshot 4Greeter screenshot 4

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
}
Something went wrong with that request. Please try again.