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:
- 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.
- 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.
- 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:
- We saw how Scala code not relying on external or Java APIs can be compiled into JavaScript.
- We saw how to use
npm
packages in a Scala.js action, by passing in an explicit reference torequire
. - We saw how to turn callback-based asynchronous JavaScript APIs into Scala
Future
s, through a (Scala)Promise
. - We saw how to turn a Scala
Future
into a JavaScriptPromise
, through.toJSPromise
. - We saw how to write a wrapper to make our Scala.js project usable as an OpenWhisk Node.js action.
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.
-
See for instance the Scala eXchange 2011 keynote, at 10m21s (registration required). ↩