Scala.js Write in Scala for the browser Sébastien Doeraene @sjrdoeraene Scala Days 2013, June 12th LAMP, lamp.epfl.ch

École polytechnique fédérale de Lausanne, Switzerland

JavaScript The one and only language of the Web

A scripting language, designed for programming in the small

How to scale to Rich Internet Applications?

JavaScript as a target language Compile a higher-level language to JavaScript GWT, Scala/GWT

CoffeeScript, Dart, TypeScript, etc.

Emscripten

js.scala

JavaScript is the Assembly language of the Web

Scala.js Scala, a scalable language

Compile to JavaScript

Interoperate with JavaScript libraries

What does it look like? (JS) var args = document.location.search.substring(1).split('&'); argsParsed = {}; for (var i = 0; i < args.length; i++) { var arg = decodeURIComponent(args[i]); if (arg.indexOf('=') == -1) { argsParsed[arg.trim()] = "true"; } else { kvp = arg.split('='); argsParsed[kvp[0].trim()] = kvp[1].trim(); } }

What does it look like? (Scala.js) val args = g.document.location.search.substring(1).split("&") .asScalaArray[String] val argsParsed = for { arg <- (args: Array[String]).toList } yield { g.decodeURIComponent(arg).split("=").asScalaArray[String] match { case Array(name, value) => name.trim -> value.trim case Array(name) => name.trim -> "true" } }

Hello World! package examples.hello object HelloWorld { def sayHelloWithPrintln() { println("Hello world!") } } Run SJS.m["examples.hello.HelloWorld"].sayHelloWithPrintln(); (Open your Web console and click the Run button to see the result.) Using console.log import scala.js object HelloWorld { def sayHelloWithConsoleLog() { js.Dynamic.global.console.log("Hello world!") } } Run SJS.m["examples.hello.HelloWorld"].sayHelloWithConsoleLog(); If your browser supports Source maps, activate them. Note that the reported position for the log points to the original .scala file! Using alert import scala.js.Dynamic.global object HelloWorld { def sayHelloWithConsoleLog() { global.alert("Hello world!") } } Run SJS.m["examples.hello.HelloWorld"].sayHelloWithAlert(); Using DOM def sayHelloWithDOM() { val document = global.document val paragraph = document.createElement("p") // paragraph.innerHTML = "Hello world!" // currently buggy paragraph.updateDynamic("innerHTML")("Hello World!") document.getElementById("helloworld-dom").appendChild(paragraph) } Run SJS.m["examples.hello.HelloWorld"].sayHelloWithDOM(); Using DOM and some input Enter your name: def sayHelloWithDOMInput() { val name = document.getElementById("helloworld-dom-input-name").value val paragraph = document.createElement("p") paragraph.updateDynamic("innerHTML")("Hello " + name + "!") document.getElementById("helloworld-dom-input").appendChild(paragraph) } Run SJS.m["examples.hello.HelloWorld"].sayHelloWithDOMInput(); Using DOM and some input given to it Enter your name: def sayHelloWithDOMInput(name: String) { val paragraph = document.createElement("p") paragraph.updateDynamic("innerHTML")("Hello " + name + "!") document.getElementById("helloworld-dom-input2").appendChild(paragraph) } Run SJS.m["examples.hello.HelloWorld"].sayHelloWithDOMInput( document.getElementById("helloworld-dom-input2-name").value); Using jQuery Enter your name: def sayHelloWithJQueryInput() { val name = jQuery("#helloworld-jquery-input-name").`val`() val paragraph = jQuery("<p>").html("Hello " + name + "!") jQuery("#helloworld-jquery-input").append(paragraph) } Run SJS.m["examples.hello.HelloWorld"].sayHelloWithJQuery(); Using jQuery and an event jQuery("#helloword-jquery-event-button").click { () => val paragraph = jQuery("<p>").html("Hello World!") jQuery("#helloworld-jquery-event").append(paragraph) } Run

Static or dynamic typing? Mix of static and dynamic typing for JS interop

All the previous examples were dynamically typed

Let us have some static typing goodness!

JS class hierarchy

Typing jQuery

Using the typed version of jQuery object jQuery extends js.Object { ... } trait JQuery extends js.Object { ... } jQuery("#typedhelloword-jquery-event-button").click { () => val paragraph = jQuery("<p>").html("Typed Hello World!") jQuery("#typedhelloword-jquery-event").append(paragraph) } Run Compile error jQuery("#typedhelloword-jquery-event-button").click { () => val paragraph = jQuery("<p>").htl("Typed Hello World!") jQuery("#typedhelloword-jquery-event").append(paragraph) } ./code/src/main/scala/examples/typedhello/HelloWorld.scala:7: value htl is not a member of examples.typedhello.JQuery val paragraph = jQuery("<p>").htl("Typed Hello World!") ^ one error found Compile error (2) jQuery("#typedhelloword-jquery-event-button").click { () => val paragraph = jQuery("<p>").html() jQuery("#typedhelloword-jquery-event").append(paragraph) } ./code/src/main/scala/examples/typedhello/HelloWorld.scala:8: type mismatch; found : scala.js.String required: examples.typedhello.JQuery jQuery("#typedhelloworld-jquery-event").append(paragraph) ^ one error found Runtime exception jQuery("#runtime-error-button").click { () => val paragraph = jQuery("<p").html("Typed Hello World!") jQuery("#runtime-error").append(paragraph) } Run Use the Pause on exception feature of your browser to explore the stack trace. If source maps are enabled, you can explore the stack in the original .scala files.

Typing jQuery object jQuery extends js.Object { def apply(selector: js.Any): JQuery = ??? } trait JQuery extends js.Object { def html(): js.String def html(value: js.String): this.type def `val`(): js.String def `val`(value: js.String): this.type def append(children: JQuery): this.type def click[U](f: js.Function0[U]): this.type } Typing the DOM object window extends js.GlobalScope { val document: DOMDocument = ??? def alert(msg: js.String): Unit = ??? } trait DOMDocument extends js.Object { def getElementById(id: js.String): DOMElement def createElement(tag: js.String): DOMElement } trait DOMElement extends js.Object { var innerHTML: js.String def appendChild(child: DOMElement): Unit }

Do we have to write that by hand? Yes ... and no. Already exists for other languages, e.g., TypeScript https://github.com/borisyankov/DefinitelyTyped We can import such type definitions (almost) automatically from TypeScript .d.ts definition files! (Work in progress)

JavaScript calling Scala.js object JSCallingScalaJS { def foo(): Int = 42 def foo(x: Int): Int = x+3 def foo(x: String): Int = x.toInt def foo(x: Any): String = x.toString def foo[A](x: Seq[A]): Int = x.size } var o = SJS.m["examples.JSCallingScalaJS"]; var result = [ o.foo(), o.foo(5), o.foo("123"), o.foo(new SJS.classes["scala.Tuple2"].jsconstructor("one", 2)), o.foo(SJS.m["scala.collection.immutable.Nil"]["::"](false)) ]; jQuery("#js-calling-scala-js-results").text(result.join(" - ")); Run

So ... Scala.js lets me write JavaScript code with Scala syntax and some static typing. Cool. Er ... what's the point? All the usual Scala goodness, of course!

Support all of Scala Classes, traits, case classes

Type system

Collections

Pattern matching

For comprehensions

And whatever it is you like most in Scala! Some samples of the Reversi demo. class Square(val x: Int, val y: Int) { private var _owner: OptPlayer = NoPlayer var onOwnerChange: (OptPlayer, OptPlayer) => Unit = (oldP, newP) => () def owner = _owner def owner_=(value: OptPlayer) { val previous = _owner _owner = value onOwnerChange(previous, value) } override def toString() = "Square("+x+", "+y+", "+owner+")" } val board = Array.tabulate[Square](BoardSize, BoardSize)(new Square(_, _)) val allSquares = board.flatten var currentPlayer: Player = White boardCanvas.click { (event: JQueryEvent) => val offsetX = event.pageX - boardCanvas.offset().left val offsetY = event.pageY - boardCanvas.offset().top val x = offsetX.toInt / SquareSizePx val y = offsetY.toInt / SquareSizePx if (inBounds(x, y)) clickSquare(board(x)(y)) } def clickSquare(square: Square) { val toFlip = computeFlips(square) if (!toFlip.isEmpty) { (square :: toFlip) foreach (_.owner = currentPlayer) nextTurn() } } def computeFlips(square: Square): List[Square] = { if (square.owner != NoPlayer) Nil else { for { i <- (-1 to 1).toList j <- -1 to 1 if i != 0 || j != 0 flip <- computeFlipsInDirection(square.x, square.y, i, j) } yield flip } } def computeScore(): (Int, Int) = { allSquares.foldLeft((0, 0)) { case ((white, black), square) => square.owner match { case White => (white+1, black) case Black => (white, black+1) case NoPlayer => (white, black) } } } val context = domCanvas.getContext("2d") .asInstanceOf[CanvasRenderingContext2D] def drawSquare(square: Square) { val x: js.Number = square.x * SquareSizePx val y: js.Number = square.y * SquareSizePx context.fillStyle = "green" context.fillRect(x, y, SquareSizePx, SquareSizePx) ... if (square.owner != NoPlayer) { context.fillStyle = if (square.owner == White) "white" else "black" context.beginPath() context.arc(x+HalfSquareSizePx, y+HalfSquareSizePx, PawnRadiusPx, 0, 2*Math.PI, true) context.fill() } }

Feature summary Complete Scala

Call JavaScript libraries Typed or untyped Import type descriptions from TypeScript Even pass on anonymous functions

Let JavaScript call you Including overloading



Main issue: code size Scala standard library = 16 MB of JavaScript code after compression by Google Closure's simple optimizations. We're striving to reduce this by all means necessary.

Future work Tooling support: sbt, IDE

Scala.js REPL in the browser

Some UI DSL

Scala.js and Play

Akka in Scala.js

Explore state-of-the-art typing systems for JavaScript