Tobias Roland

I'm a London based software engineer, currently taking a few months out of my career to study functional programming. This is my blog.

GCP Cloud Functions with Scala.js in 10 lines(ish)

17 November 2019

Update: I’ve since this blog post created a giter8 template, so if you just want to get up and running, execute:

sbt new TobiasRoland/scalajs-gcp-cloud-functions.g8

It’s easy to spin up a node.js runtime on Google Cloud Platform’s Cloud Functions, but say you want to write in Scala instead of JavaScript… well, that’s thankfully easy too! Though requires a few extra steps.

Since couldn’t find a good tutorial on how to get Scala.js working in a google Cloud Function, I decided to write one.

What we’ll build

An earth-shatteringly simple cloud function in Scala(.js) that will respond to any HTTP request with a 200 OK and a plaintext body of Hello World.

This tutorial is written to be accessible to beginners, and so you shouldn’t need any deep familiarity with scala.js, sbt, GCP, cloud functions nor node.js to follow this example.

In the end, we’ll have about 10 lines of scala code and approximately the same amount of sbt configuration - that’s all it takes. There’s a few interesting things that are worth paying attention to within those 20 lines, so let’s get to it.

Note: The latest version of the code in this tutorial example can be found on GitHub.

Why would I want to do this in the first place?

Let’s say the work you want to accomplish is stateless and short lived. Spinning up a JVM for a single request and then killing it again is an overhead you don’t necessarily want.

Unlike AWS Lambda and Azure Functions, GCP Cloud Functions do not support running native JVM applications their serverless offering as of Jan 2020. There are other ways to use the JVM on GCP, though these require significantly more configuration and know-how of GCP.

By transpiling your code to JavaScript, you won’t have to pay the cold-JVM startup costs. While it’s tempting to jumpt to the conclusion that transpiling all of scala into javascript would result in a huge slow artifact, the optimization step makes sure to only transpile the bare minimum amount of code required to execute the function.

Preconditions

I assume the following have been installed already:

  • sbt
  • nodejs

Tutorial

We’ll start with a vanilla sbt project:

hello-world
├── build.sbt
├── project
│   └── plugins.sbt
└── src
    └── main
        └── scala

Add the scala.js sbt plugin:

In your plugins.sbt, declare that you want to use the scala.js plugin (latest version can be found here):

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

Add your sbt configuration:

In your build.sbt, you will need to enable the plugin and set a few important settings.

name := "hello-world"
version := "0.1"
scalaVersion := "2.12.10"
enablePlugins(ScalaJSPlugin)
scalaJSModuleKind := org.scalajs.core.tools.linker.backend.ModuleKind.CommonJSModule
scalaJSUseMainModuleInitializer := false

// NOTE! To use dependencies with scala.js, dependencies are declared with triple percentage instead of double
libraryDependencies += "io.scalajs.npm" %%% "express" % "0.4.2"

What we’ve accomplished here:

enablePlugins(ScalaJSPlugin) does what it says on the tin

scalaJSModuleKind has been set to CommonJSModule. This is the module system of NodeJS (our runtime), and allows us to export our methods so they can be invoked by GCP

scalaJSUseMainModuleInitializer is set to false as we won’t be using a “main app”. We want to compile all of our code into a single .js file and expose an exported JS-function that can be invoked by GCP

express dependency. This is the library we use to handle the HTTP Request and Response. If you’ve ever created a GCP Cloud Function from Google’s tutorial on writing a Slack command with GCP Functions in JS, it is indeed the very same npm library wrapped in a thin shim of scala.

Note: At the time of writing, latest version of express is "io.scalajs.npm" %%% "express" % "0.5" though there appears to be an issue with the dependency not resolving - try the latest version out, and if you’re having issues, fall back to an earlier version.

HelloWorldExample.scala

Enough setup - now, let’s write the actual code that will respond with Hello World when receiving and http request.

Create the scala source file, src/scala/codes/mostly/gcp/cloudfunctions/HelloWorldExample.scala, and populate it with some imports:

import io.scalajs.npm.express.{Request, Response}
import scala.scalajs.js.{Function2 => JSFunction2}
import scala.scalajs.js.annotation.JSExportTopLevel

We will use Response and Request from the express dependency mentioned earlier, and we’ll also import the scalajs Function2 as JSFunction2. You strictly speaking don’t have to do this renaming, but I like to make that distinction since the standard scala library already has a Function2, and it’s nice to keep the distinction between regular scala code and the explicitly JS-code. Lastly, we’ve got an import for the JSExportTopLevel annotation - we’ll come to that in a bit.

Now for the main logic. We need to provide a JSFunction2 that can be called by GCP:

object HelloWorldExample {
  val helloWorld: JSFunction2[Request, Response, Unit] = (req, res)  {
    res.status(200).send("Hello World")
  }
}

The function itself is trivial. It’s interesting to note the right-hand side of the = is just regular scala code. The code is automatically (well, implicitly) converted into a JSFunction2!

Finally, we need to make use of the JSExportTopLevel annotation:

object HelloWorldExample {
  @JSExportTopLevel("helloWorld")
  val helloWorld: JSFunction2[Request, Response, Unit] = (req, res)  {
    res.status(200).send("Hello World")
  }
}

Adding the annotation specifies that the function should be exported to the top level of the module, and will thus be callable from GCP as “helloWorld” (or whatever you decided to put as the parameter). The string doesn’t strictly have to match the name of the val, but I see no reason not to have them be identical.

Putting it all together, we end up with:

package codes.mostly.gcp.cloudfunctions

import io.scalajs.npm.express.{Request, Response}
import scala.scalajs.js.annotation.JSExportTopLevel
import scala.scalajs.js.{Function2 => JSFunction2}

object HelloWorldExample {
  @JSExportTopLevel("helloWorld")
  val helloWorld: JSFunction2[Request, Response, Unit] = (req, res)  {
    res.status(200).send("Hello World")
  }
}

Exactly ten lines of scala if you ignore the whitespace.

Build it

Since we’re using the sbt scala.js plugin, we can now run a one-line command to build our JS file:

$ sbt fullOptJS

This will generate our .js file in the target folder. If the name of the project is hello-world, the file will be named /target/scala-2.12/hello-world-opt.js. Because we used fullOptJS to fully optimize the JS, it should’ve been minimized into javascript gibberish:

'use strict';
'use strict';var e="object"===typeof __ScalaJSEnv&&__ScalaJSEnv?__ScalaJSEnv:{},k="object"===typeof e.global&&e.global?e.global:"object"===typeof global&&global&&global.Object===Object?global:this;e.global=k;var m=exports;e.exportsNamespace=m;k.Object.freeze(e);var q={envInfo:e,semantics:{asInstanceOfs:2,arrayIndexOutOfBounds:2,moduleInit:2,strictFloats:!1,productionMode:!0},assumingES6:!1,linkerVersion:"0.6.31",globalThis:this};k.Object.freeze(q);k.Object.freeze(q.semantics);
var r=k.Math.imul||function(a,b){var c=a&65535,d=b&65535;return c*d+((a>>>16&65535)*d+c*(b>>>16&65535)<<16>>>0)|0},t=k.Math.clz32||function(a){if(0===a)return 32;var b=1;0===(a&4294901760)&&(a<<=16,b+=16);0===(a&4278190080)&&(a<<=8,b+=8);0===(a&4026531840)&&(a<<=4,b+=4);0===(a&3221225472)&&(a<<=2,b+=2);return b+(a>>31)},u=0,v=k.WeakMap?new k.WeakMap:null;function w(a){return function(b,c){return!(!b||!b.$classData||b.$classData.k!==c||b.$classData.i!==a)}}function aa(a){for(var b in a)return b}
function x(a,b,c){var d=new a.A(b[c]);if(c<b.length-1){a=a.l;c+=1;for(var h=d.y,g=0;g<h.length;g++)h[g]=x(a,b,c)}return d}function ba(a){switch(typeof a){case "string":return y(z);case "number":var b=a|0;return b===a?A(b)?y(B):C(b)?y(D):y(E):"number"===typeof a?y(F):y(G);case "boolean":return y(I);case "undefined":return y(ca);default:return null===a?a.T():a instanceof J?y(da):a&&a.$classData?y(a.$classData):null}}
function ea(a){switch(typeof a){case "string":fa||(fa=(new K).d());for(var b=0,c=1,d=-1+(a.length|0)|0;0<=d;)b=b+r(65535&(a.charCodeAt(d)|0),c)|0,c=r(31,c),d=-1+d|0;return b;case "number":L||(L=(new M).d());b=L;c=a|0;if(c===a&&-Infinity!==1/a)b=c;else{if(b.f)b.B[0]=a,b=N(b.r[b.D]|0,b.r[b.C]|0);else{if(a!==a)b=!1,a=2047,c=+k.Math.pow(2,51);else if(Infinity===a||-Infinity===a)b=0>a,a=2047,c=0;else if(0===a)b=-Infinity===1/a,c=a=0;else if(d=(b=0>a)?-a:a,d>=+k.Math.pow(2,-1022)){a=+k.Math.pow(2,52);c=
+k.Math.log(d)/.6931471805599453;c=+k.Math.floor(c)|0;c=1023>c?c:1023;var h=+k.Math.pow(2,c);h>d&&(c=-1+c|0,h/=2);h=d/h*a;d=+k.Math.floor(h);h-=d;d=.5>h?d:.5<h?1+d:0!==d%2?1+d:d;2<=d/a&&(c=1+c|0,d=1);1023<c?(c=2047,d=0):(c=1023+c|0,d-=a);a=c;c=d}else a=d/+k.Math.pow(2,-1074),c=+k.Math.floor(a),d=a-c,a=0,c=.5>d?c:.5<d?1+c:0!==c%2?1+c:c;c=+c;b=N(c|0,(b?-2147483648:0)|(a|0)<<20|c/4294967296|0)}b=b.s^b.q}return b;case "boolean":return a?1231:1237;case "undefined":return 0;default:return a&&a.$classData||
null===a?a.w():null===v?42:ha(a)}}var ha=null!==v?function(a){switch(typeof a){case "string":case "number":case "boolean":case "undefined":return ea(a);default:if(null===a)return 0;var b=v.get(a);void 0===b&&(u=b=u+1|0,v.set(a,b));return b}}:function(a){if(a&&a.$classData){var b=a.$idHashCode$0;if(void 0!==b)return b;if(k.Object.isSealed(a))return 42;u=b=u+1|0;return a.$idHashCode$0=b}return null===a?0:ea(a)};function A(a){return"number"===typeof a&&a<<24>>24===a&&1/a!==1/-0}
function C(a){return"number"===typeof a&&a<<16>>16===a&&1/a!==1/-0}function O(){this.t=this.A=void 0;this.i=this.l=this.h=null;this.k=0;this.z=null;this.p="";this.b=this.n=this.o=void 0;this.name="";this.isRawJSType=this.isArrayClass=this.isInterface=this.isPrimitive=!1;this.isInstance=void 0}function P(a,b,c){var d=new O;d.h={};d.l=null;d.z=a;d.p=b;d.b=function(){return!1};d.name=c;d.isPrimitive=!0;d.isInstance=function(){return!1};return d}
function Q(a,b,c,d,h,g,l){var f=new O,n=aa(a);g=g||function(p){return!!(p&&p.$classData&&p.$classData.h[n])};l=l||function(p,H){return!!(p&&p.$classData&&p.$classData.k===H&&p.$classData.i.h[n])};f.t=h;f.h=c;f.p="L"+b+";";f.b=l;f.name=b;f.isInterface=!1;f.isRawJSType=!!d;f.isInstance=g;return f}
function ia(a){function b(f){if("number"===typeof f){this.y=Array(f);for(var n=0;n<f;n++)this.y[n]=h}else this.y=f}var c=new O,d=a.z,h="longZero"==d?R().u:d;b.prototype=new S;b.prototype.constructor=b;b.prototype.$classData=c;d="["+a.p;var g=a.i||a,l=a.k+1;c.A=b;c.t=ja;c.h={a:1,V:1,c:1};c.l=a;c.i=g;c.k=l;c.z=null;c.p=d;c.o=void 0;c.n=void 0;c.b=void 0;c.name=d;c.isPrimitive=!1;c.isInterface=!1;c.isArrayClass=!0;c.isInstance=function(f){return g.b(f,l)};return c}
function y(a){if(!a.o){var b=new T;b.m=a;a.o=b}return a.o}O.prototype.getFakeInstance=function(){if(this===z)return"some string";if(this===I)return!1;if(this===B||this===D||this===E||this===F||this===G)return 0;if(this===da)return R().u;if(this!==ca)return{$classData:this}};O.prototype.getSuperclass=function(){return this.t?y(this.t):null};O.prototype.getComponentType=function(){return this.l?y(this.l):null};
O.prototype.newArrayOfThisClass=function(a){for(var b=this,c=0;c<a.length;c++)b.n||(b.n=ia(b)),b=b.n;return x(b,a,0)};var ka=P(!1,"Z","boolean"),la=P(0,"C","char"),ma=P(0,"B","byte"),na=P(0,"S","short"),oa=P(0,"I","int"),pa=P("longZero","J","long"),qa=P(0,"F","float"),ra=P(0,"D","double");ka.b=w(ka);la.b=w(la);ma.b=w(ma);na.b=w(na);oa.b=w(oa);pa.b=w(pa);qa.b=w(qa);ra.b=w(ra);function U(){}function S(){}S.prototype=U.prototype;U.prototype.d=function(){return this};U.prototype.x=function(){var a=ba(this).m.name,b=(+(this.w()>>>0)).toString(16);return a+"@"+b};U.prototype.w=function(){return ha(this)};U.prototype.toString=function(){return this.x()};var ja=Q({a:0},"java.lang.Object",{a:1},void 0,void 0,function(a){return null!==a},function(a,b){if(a=a&&a.$classData){var c=a.k||0;return!(c<b)&&(c>b||!a.i.isPrimitive)}return!1});U.prototype.$classData=ja;function V(){}
V.prototype=new S;V.prototype.constructor=V;V.prototype.d=function(){W=this;sa=function(a,b){W||(W=(new V).d());b.status(200).send("Hello World")};return this};V.prototype.$classData=Q({E:0},"codes.mostly.gcp.cloudfunctions.HelloWorldExample$",{E:1,a:1});var W=void 0;function T(){this.m=null}T.prototype=new S;T.prototype.constructor=T;T.prototype.x=function(){return(this.m.isInterface?"interface ":this.m.isPrimitive?"":"class ")+this.m.name};T.prototype.$classData=Q({I:0},"java.lang.Class",{I:1,a:1});
function M(){this.f=!1;this.B=this.r=this.j=null;this.v=!1;this.D=this.C=0}M.prototype=new S;M.prototype.constructor=M;
M.prototype.d=function(){L=this;this.j=(this.f=!!(k.ArrayBuffer&&k.Int32Array&&k.Float32Array&&k.Float64Array))?new k.ArrayBuffer(8):null;this.r=this.f?new k.Int32Array(this.j,0,2):null;this.f&&new k.Float32Array(this.j,0,2);this.B=this.f?new k.Float64Array(this.j,0,1):null;if(this.f)this.r[0]=16909060,a=1===((new k.Int8Array(this.j,0,8))[0]|0);else var a=!0;this.C=(this.v=a)?0:1;this.D=this.v?1:0;return this};M.prototype.$classData=Q({O:0},"scala.scalajs.runtime.Bits$",{O:1,a:1});var L=void 0;
function K(){}K.prototype=new S;K.prototype.constructor=K;K.prototype.d=function(){return this};K.prototype.$classData=Q({R:0},"scala.scalajs.runtime.RuntimeString$",{R:1,a:1});var fa=void 0;function X(){}X.prototype=new S;X.prototype.constructor=X;function ta(){}ta.prototype=X.prototype;var ca=Q({S:0},"scala.runtime.BoxedUnit",{S:1,a:1,c:1},void 0,void 0,function(a){return void 0===a}),I=Q({G:0},"java.lang.Boolean",{G:1,a:1,c:1,e:1},void 0,void 0,function(a){return"boolean"===typeof a});
function Y(){this.u=null}Y.prototype=new S;Y.prototype.constructor=Y;Y.prototype.d=function(){Z=this;this.u=N(0,0);return this};
function ua(a,b){if(0===(-2097152&b))b=""+(4294967296*b+ +(a>>>0));else{var c=(32+t(1E9)|0)-(0!==b?t(b):32+t(a)|0)|0,d=c,h=0===(32&d)?1E9<<d:0;d=0===(32&d)?5E8>>>(31-d|0)|0|0<<d:1E9<<d;var g=a,l=b;for(a=b=0;0<=c&&0!==(-2097152&l);){var f=g,n=l,p=h,H=d;if(n===H?(-2147483648^f)>=(-2147483648^p):(-2147483648^n)>=(-2147483648^H))f=l,n=d,l=g-h|0,f=(-2147483648^l)>(-2147483648^g)?-1+(f-n|0)|0:f-n|0,g=l,l=f,32>c?b|=1<<c:a|=1<<c;c=-1+c|0;f=d>>>1|0;h=h>>>1|0|d<<31;d=f}c=l;if(0===c?-1147483648<=(-2147483648^
g):-2147483648<=(-2147483648^c))c=4294967296*l+ +(g>>>0),g=c/1E9,h=g/4294967296|0,d=b,b=g=d+(g|0)|0,a=(-2147483648^g)<(-2147483648^d)?1+(a+h|0)|0:a+h|0,g=c%1E9|0;c=""+g;b=""+(4294967296*a+ +(b>>>0))+"000000000".substring(c.length|0)+c}return b}Y.prototype.$classData=Q({Q:0},"scala.scalajs.runtime.RuntimeLong$",{Q:1,a:1,W:1,c:1});var Z=void 0;function R(){Z||(Z=(new Y).d());return Z}
var z=Q({F:0},"java.lang.String",{F:1,a:1,c:1,U:1,e:1},void 0,void 0,function(a){return"string"===typeof a}),B=Q({H:0},"java.lang.Byte",{H:1,g:1,a:1,c:1,e:1},void 0,void 0,function(a){return A(a)}),G=Q({J:0},"java.lang.Double",{J:1,g:1,a:1,c:1,e:1},void 0,void 0,function(a){return"number"===typeof a}),F=Q({K:0},"java.lang.Float",{K:1,g:1,a:1,c:1,e:1},void 0,void 0,function(a){return"number"===typeof a}),E=Q({L:0},"java.lang.Integer",{L:1,g:1,a:1,c:1,e:1},void 0,void 0,function(a){return"number"===
typeof a&&(a|0)===a&&1/a!==1/-0}),da=Q({M:0},"java.lang.Long",{M:1,g:1,a:1,c:1,e:1},void 0,void 0,function(a){return a instanceof J}),D=Q({N:0},"java.lang.Short",{N:1,g:1,a:1,c:1,e:1},void 0,void 0,function(a){return C(a)});function J(){this.q=this.s=0}J.prototype=new ta;J.prototype.constructor=J;J.prototype.x=function(){R();var a=this.s,b=this.q;return b===a>>31?""+a:0>b?"-"+ua(-a|0,0!==a?~b:-b|0):ua(a,b)};function N(a,b){var c=new J;c.s=a;c.q=b;return c}J.prototype.w=function(){return this.s^this.q};
J.prototype.$classData=Q({P:0},"scala.scalajs.runtime.RuntimeLong",{P:1,g:1,a:1,c:1,e:1});var sa=null;W||(W=(new V).d());Object.defineProperty(m,"helloWorld",{get:function(){return sa},configurable:!0});
//# sourceMappingURL=hello-world-opt.js.map

An aesthetic nightmare, but a clever piece of optimization! You will note that despite us transpiling scala into JS, the end result is surprisingly a small amount of Javascript. This is thanks to the optimizer dutifully identifying the classes and methods reachable from our entry point and removing everything else. Theoretically, using fullOptJS instead of fastOptJS should also result in a more “thorough” optimization resulting in faster code. The actual performance improvement is in all likelihood negligible, but since we’re treating javascript as a transpilation target, readability doesn’t matter to us.

Wrapping up + next steps

… And that’s it! All you’ve got left is deploying the scala.js output javascript as a Cloud Function. For that, I refer you to the excellent GCP Cloud Functions documentation.

Obviously, the Hello World example is too simple to justify a transpiled scala.js project, but for projects where you want type safety and the scala eco system present, I think tiny amount of boilerplate and dependencies required to get up and running makes it worthwhile.

Word of warning; as you add more dependencies and lines of code to your project, the JS payload size will start to shoot up rather quickly, and (depending on complexity of your code) so will the execution time. If your function ends up taking a significant amount of time to execute in GCP, you will probably want to consider other solutions than serverless functions as your costs are directly tied to the execution time of the function.


If you found this useful, have a look at the code on github and hey, while you’re there, maybe add a ⭐ to give me that dopamine hit of validation!

Update: A giter8 template is now available: TobiasRoland/scalajs-gcp-cloud-functions.g8

This tutorial is just a primer. In a future post, I’ll write up a more involved example based on this simple skeleton.

Thanks for reading!