psuter.net

1-555-OPENWSK

This post first appeared on an external blog. To read more about OpenWhisk, head to the developerWorks OpenWhisk blog.

In this post, we demonstrate with two examples how to write OpenWhisk actions in Scala.

Can’t wait to try it out yourself? The full code for both samples is available on GitHub with instructions for building and deployment. For a detailed description of the code and developement process, keep reading.

Pick a runtime

OpenWhisk supports a variety of runtimes, allowing developers to upload actions written in JavaScript, Swift, Java, and Python. It additionally supports actions built as Docker images and uploaded to Docker Hub. While OpenWhisk has currently no dedicated support for Scala, we can list at least three ways to achieve our goal:

  1. As a Java action. Because Java actions are created by uploading bytecode, there is in principle no distinction between Scala and Java. This approach however would require packaging the Scala standard library with the action (or at least the parts required to run it), which is impractical.
  2. As a blackbox action (Docker image). This approach offers full flexibility in the choice of runtime, but requires the developer to expose the action as an HTTP endpoint. Also, because the action image needs to be updated to Docker Hub, development iterations are longer.
  3. As a JavaScript action. Through Scala.js, it is possible to compile Scala straight into JavaScript.

In light of the limitations highlighted above, we argue that Scala.js is the best choice. In the rest of this post, we use it to build a minimal Scala action, then describe a more advanced example, including asynchronous computations and the use of an npm package.

Note that while this post should be understandable by anyone familiar with OpenWhisk, JavaScript, and Scala, it is not an introduction to Scala.js. We recommend interested readers to follow the excellent Scala.js basic tutorial if needed.

How do you do?

We start with a very simple action which, in the grand tradition of progamming first steps, greets the user. The recommended build tool for Scala.js is sbt and we do not deviate from this advice. Our project has the following layout:

build.sbt
project/plugins.sbt
src/main/scala/hello/Main.scala

The first file contains the following three lines:

enablePlugins(ScalaJSPlugin)
name := "hello-whisk"
scalaVersion := "2.11.8"

The second one makes the Scala.js plugin available, and contains only one line:

addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.11")

Finally, the third one contains the code for our action itself:

package hello

import scala.scalajs.js
import scala.scalajs.js.annotation.JSExport

@JSExport
object Main {
    @JSExport
    def main(args: js.Dictionary[js.Any]) : js.Dictionary[js.Any] = {
        val name = args.getOrElse("name", "stranger").toString
        js.Dictionary("greeting" -> s"Hello $name!")
    }
}

Note that we did not extend js.JSApp. OpenWhisk actions aren’t applications so much as they are functions. In particular, they always expect a single (dictionary) argument and return a dictionary. This property is reflected in the signature we chose for main. Note as well the uses of @JSExport, which ensure that after compilation into JavaScript, the annotated symbols are preserved.

If we built the action now, e.g. with sbt fullOptJS, the resulting file would not yet be usable as an OpenWhisk action. Indeed, actions require a top-level main function, but our function is only accessible as hello.Main().main. We address this issue by appending a top-level function after the generated code. To do it automatically for each build, we add the following to our build.sbt:

scalaJSOutputWrapper := ("", """
// OpenWhisk action entrypoint
function main(args) {
  return hello.Main().main(args);
}
""")

We are now ready to build, deploy, and invoke our first Scala.js OpenWhisk action:

$ sbt fullOptJS
$ wsk action create hello-scala target/scala-2.11/hello-whisk-opt.js
ok: created action hello-scala
$ wsk action invoke -br hello-scala -p name reader
{
    "greeting": "Hello reader!"
}

…and our first proof-of-concept is complete.

Dial now!

To illustrate some finer points, we now port the “phone mnemonics” problem to an OpenWhisk action. For context, this problem was drawn from the paper An Empirical Comparison of Seven Programming Languages, a 2000 academic study on programming language effectiveness, and can be stated as follows:

Given a sequence of digits, a list of words, and assuming a standard keypad layout, find all the word sequences that the digit sequence can encode.

For instance, the digits 673694475 can encode the sequences [ open, whisk ] or [ mr, fox, girl ], or stranger ones yet.

A Scala version was written by Martin Odersky, the creator of the language, and used in several presentations to showcase the conciseness and expressivity of the language.1 Typed up versions can be found online, and we base our OpenWhisk action on such a transcription:

class Coder(words: List[String]) {

  private val mnemonics = Map(
      '2' -> "ABC", '3' -> "DEF", '4' -> "GHI", '5' -> "JKL", 
      '6' -> "MNO", '7' -> "PQRS", '8' -> "TUV", '9' -> "WXYZ")

  /** Invert the mnemonics map to give a map from chars 'A' ... 'Z' to '2' ... '9' */
  private val charCode: Map[Char, Char] = 
    for ((digit, str) <- mnemonics; letter <- str) yield letter -> digit

  /** Maps a word to the digit string it can represent, e.g. "Java” -> "5282” */
  private def wordCode(word: String): String = word.toUpperCase map charCode

  /** A map from digit strings to the words that represent them, 
    * e,g. "5282" -> List("Java", "Kata", "Lava", ...) 
    * Note: A missing number should map to the empty set, e.g. "1111" -> List()
    */
  private val wordsForNum: Map[String, Seq[String]] = (words groupBy wordCode) withDefaultValue List()
 
  /** Return all ways to encode a number as a list of words */
  def encode(number: String): Set[List[String]] = 
    if (number.isEmpty) Set(List())
    else {
      for {
        split <- 1 to number.length
        word <- wordsForNum(number take split)
        rest <- encode(number drop split)
      } yield word :: rest
    }.toSet

  /** Maps a number to a list of all word phrases that can represent it */
  def translate(number: String): Set[String] = encode(number) map (_ mkString " ")
}

To obtain a set of possible encoded sequences from a digit string number, we execute new Coder(words).translate(number). This leaves us with the problem of obtaining the list words. The original example parses the file /usr/share/dict/words, which contains a list of English words and is conveniently present on many Unix-like systems. OpenWhisk actions however run in a containerized environment and we cannot assume that such a file will be accessible. As an alternative source, we use instead a sample words file available online and linked from the Wikipedia page on the topic.

Requesting assistance

Downloading the list of words requires making an HTTP request. We cannot use standard Scala libraries for this task as they all depend on JVM-specific functionality unavailable when running on Node.js (e.g. java.nio.*). Instead, we can rely on packages from npm: OpenWhisk actions by default have access to a number of such packages, including request, for issuing HTTP requests.

Loading packages in JavaScript is achieved through the use of the special function require. Due to the unusual way in which Node.js makes it available in the global scope, require cannot be accessed from Scala.js through js.Dynamic.global, and must be passed in explicitly from a JavaScript context. As we have seen in our first action example, our OpenWhisk actions come with a JavaScript wrapper anyway, and we will see later how to augment it to solve the require issue. For now, we simply assume that our Scala.js code has access to a variable require of type js.Dynamic.

As is standard in JavaScript, request works asynchronously, and triggers a callback when its work is done. The naturally corresponding construct in Scala is a Future. To return a Future from a JavaScript call using a callback, we can take a detour through a Scala Promise: the JavaScript callback fulfills or fails it according to the desired logic.

With all above considerations in mind, we obtain the following helper function to asynchronously load a list of words from a URL:

def loadWords(require: js.Dynamic) : Future[List[String]] = {
  val request = require("request")
  val bodyPromise = Promise[String]

  request("https://users.cs.duke.edu/~ola/ap/linuxwords", (error: js.Dynamic, response: js.Dynamic, body: js.Dynamic) => {
    if(!error && response.statusCode == 200) {
      bodyPromise.trySuccess(body.toString)
    } else {
      bodyPromise.tryFailure(new Exception("Request to word list failed."))
    }
  })

  bodyPromise.future.map { b =>
    b.split("\n").map(_.trim).filter(_.matches("[a-zA-Z]+")).toList
  }
}

You promised

Because our action starts with an asynchronous request, it has to produce its result asynchronously too. In OpenWhisk, the simplest way to achieve this is to return a (JavaScript) Promise. The most straightforward way to produce a JavaScript Promise from a Scala Future is to call .toJSPromise, available through import scala.scalajs.js.JSConverters._. We can then declare our main method as returning for instance js.Promise[js.Any].

Putting it all together, our Scala code is as follows:

package mnemonics

import ...

@JSExport
object Main {
  @JSExport
  def main(require: js.Dynamic, args: js.Dictionary[js.Any]) : js.Promise[js.Any] = {
    loadWords(require) map { wordList =>
      val number = args("number").toString
      val coder = new Coder(wordList)
      val results = coder.translate(number).toSeq
      js.Dictionary("phrases" -> js.Array(results : _*))
    } recover {
      case failure =>
        js.Dictionary("error" -> failure.getMessage)
    } toJSPromise
  }

  private def loadWords(require: js.Dynamic) = // as shown above
}

class Coder(words: List[String]) { ... } // as shown above

The action wrapper, declared as before through scalaJSOutputWrapper in build.sbt, now has the following definition:

// OpenWhisk action entrypoint
function main(args) {
  return mnemonics.Main().main(require, args);
}

Note that both the success and failure modes of the Scala Future are translated into their equivalent behaviors in terms of JavaScript Promise, and thus also in terms of OpenWhisk actions.

Hanging up

To summarize:

All that remains really at this point is to try it out:

$ sbt fullOptJS
$ wsk action create mnemonics-scala ./target/scala-2.11/whisk-mnemonics-opt.js
ok: created action mnemonics-scala
$ wsk action invoke -br mnemonics-scala -p number '"84265968"'                                                                                    
{
    "phrases": [
        "thank you"
    ]
}

Thanks to Sébastien Doeraene for suggesting improvements on a previous version of this post.

  1. See for instance the Scala eXchange 2011 keynote, at 10m21s (registration required).