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

EPFL logo             Scala logo

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!")
  }
}
      


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!")
  }
}
      


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!")
  }
}
      


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)
}
      


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)
}
      


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)
}
      


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)
}
      


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)
}
      

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)
}
      

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)
}
      

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(" - "));
      

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

That's all for today!

https://github.com/lampepfl/scala-js