Get the source code for this article: https://github.com/JamesEarlDouglas/imperatively
In an earlier article, I introduced a basic way to represent multi-step Web workflows in an imperative style. The example was somewhat limited, and I have since added a few features to make it more practical.

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>
}



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.
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) }
}
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
}