Sébastien Doeraene @sjrdoeraene
Scala Days 2013, June 12th
LAMP, lamp.epfl.ch
École polytechnique fédérale de Lausanne, Switzerland
Compile a higher-level language to JavaScript
JavaScript is the Assembly language of the Web
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();
}
}
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"
}
}
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)
}
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)
}
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
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
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.
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
}
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
}
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)
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!
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()
}
}
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.
That's all for today!