Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

Akka HTTP DSL in continuation-passing style

Akka HTTP provides a powerful Scala DSL to create routes to handle HTTP requests. The DSL contains a set of Directives to extract information or manipulate requests or responses.

For example, the following cpsRoute accepts a GET request and extracts query parameters p1 and p2 with the help of get and parameters directive. If an HTTP client send a GET request to the URL /?p1=Hello&p2=World, it will receive the response of plain text Hello, World!.

import akka.http.scaladsl.server._, Directives._
def cpsRoute: Route = {
  get {
    parameters("p1", "p2") { (p1, p2) =>
      complete(s"$p1, $p2!")
    }
  }
}

What is unusual from PHP or other HTTP server API is that you cannot get the result of parameters directive from the return value. Instead, you must pass a callback function to handle the result. This API style in Akka HTTP DSL is called CPS (continuation-passing style), which will cause the infamous "callback hell" problem when there are lots of directives. Since each directive requires an indentation level and a pair of curly brackets, the curly brackets can become deeply nested and the code become too complicated to read.

Escape from callback hell

Alternative to Akka HTTP's continuation-passing style DSL, I recently released the library Dsl.scala-akka-http to provide direct style DSL for Akka HTTP.

Dsl.scala-akka-http is based on Dsl.scala, which provides some compiler plugins to perform CPS-transformation. The following settings enable those compiler plugins for sbt projects.

// Common sbt settings for Dsl.scala
addCompilerPlugin("com.thoughtworks.dsl" %% "compilerplugins-bangnotation" % "1.2.0")
addCompilerPlugin("com.thoughtworks.dsl" %% "compilerplugins-reseteverywhere" % "1.2.0")

In Dsl.scala, each CPS operation can be written in direct style as a !-notation, and at compile time, the user written direct style code will be automatically translated to CPS function calls. You can check this documentation for the details of Dsl.scala and !-notation. For now, in this Akka HTTP case, the CPS operation is TApply, which can be added to sbt projects with the following settings:

// Sbt settings for the `TApply` keyword
libraryDependencies += "com.thoughtworks.dsl" %% "keywords-akka-http-tapply" % "1.0.0"

Then you can create Akka HTTP routes in direct style. For example, the following directStyleRoute provides the same functionality as cpsRoute.

import akka.http.scaladsl.server._, Directives._
import com.thoughtworks.dsl.keywords.akka.http.TApply
def directStyleRoute: Route = {
  !TApply(get)
  val (p1, p2) = !TApply(parameters("p1", "p2"))
  complete(s"$p1, $p2!")
}

Those !TApply are direct style operator to extract the result from directive as return values instead of callback function parameters.

Alternatively, you can use direct style DSL only for parameters directive, and leave get directive in CPS style, as shown in the following mixStyleRoute:

def mixStyleRoute: Route = {
  get {
    val (p1, p2) = !TApply(parameters("p1", "p2"))
    complete(s"$p1, $p2!")
  }
}

Heterogenous library defined keywords

Semantically, TApply, along with a !-notation, can be considered a library defined keyword to perform special operations that are impossible to be implemented in an ordinary Scala library. Each keyword like TApply can be used together with other keywords.

For example, you can use com.thoughtworks.dsl.keywords.Using to automatically manage resources, as shown below:

// Sbt settings for the `Using` keyword
libraryDependencies += "com.thoughtworks.dsl" %% "keywords-using" % "1.2.0"

 

import akka.http.scaladsl.server.Directives._
import com.thoughtworks.dsl.keywords.Using
import java.nio.file.Files
import java.nio.file.Paths
import scala.collection.JavaConverters._

def currentDirectoryRoute = {
  pathPrefix("current-directory") {
    get {
      val glob = !TApply(parameters("glob"))
      val currentDirectory = !Using(Files.newDirectoryStream(Paths.get(""), glob))
      path("file-names") {
        complete(currentDirectory.iterator.asScala.map(_.toString).mkString(","))
      } ~ path("number-of-files") {
        complete(currentDirectory.iterator.asScala.size.toString)
      }
    }
  }
}

With the help of the additional Using keyword, the currentDirectoryRoute will open the currentDirectory, which is a closeable DirectoryStream created from Files.newDirectoryStream(), according to the glob pattern extract from the query parameter, and automatically close the directory after the HTTP request is processed completely. For example, you will find a comma separated file list of all *.md file names of current directory at the URL /current-directory/file-names?glob=*.md, and the URL /current-directory/number-of-files?glob=*.md contains the number of *.md files in current directory. The DirectoryStream will be open and closed when processing either of the URLs.

Resource management is relatively trivial in Akka HTTP DSL, without the help of Dsl.scala, as shown below:

def akkaHttpCurrentDirectoryRoute: Route = {
  parameters("glob") { glob =>
    val currentDirectory = Files.newDirectoryStream(Paths.get(""), glob)
    mapRouteResultFuture { future =>
      future.transform { result: Try[RouteResult] =>
        Try {
          currentDirectory.close()
        }.flatMap { _: Unit =>
          result
        }
      }
    } {
      pathPrefix("current-directory") {
        get {
          path("file-names") {
            complete(currentDirectory.iterator.asScala.map(_.toString).mkString(","))
          } ~ path("number-of-files") {
            complete(currentDirectory.iterator.asScala.size.toString)
          }
        }
      }
    }
  }
}

You can compare akkaHttpCurrentDirectoryRoute with currentDirectoryRoute to learn how Dsl.scala simplifies the query parameter extraction as well as resource management.

Conclusion

In this article, I introduced Dsl.scala-akka-http, a library based on Dsl.scala to provide direct style DSL keyword TApply for Akka HTTP. There are other library-defined keywords in Dsl.scala, for collection manipulation, null-safety, exception handling, parallel execution, etc. You can find them in Dsl.scala's Scaladoc.

I created the library Dsl.scala during my career in ThoughtWorks, and I will keep maintaining this library in my part time, though I have resigned from ThoughtWorks recently. Since I am back to the job market, you can contact me if you recognize my expertise in my open source contributions. I am currently seeking for career opportunity in California, especially in the San Francisco bay area or San Diego.

Links

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.