Scala.jsを触ってみた。

nakamuraが2015/07/16 11:26:57に投稿

Scala.jsを触ってみた

Scala.jsとは

Scala.jsを使うとScalaをJavaScriptにコンパイルできます。
CoffeeScriptやTypeScriptのようなものです。
クライアント側のプログラムをScalaで実装することができ、
Scalaなのでコンパイル時に型のチェックもできます。
IntelliJ IDEAのようなScalaに対応したIDEを使えば
コードの補完などもできます。

2015年2月にリリースされたv0.6.0からはexperimantalでなくなったので試してみました。

Scala.jsのチュートリアルをやってみたのでどんな感じか紹介します。
試したバージョンは現時点で最新のv0.6.4です。

準備

Scala.jsはsbtのプラグインです。使うにはsbtが必要なので事前にインストールしておきます。
またNode.jsがあると高速にJavaScriptの実行ができるのでインストールしておいたほうがいいです。

Hello, worldの作成

まずは適当なディレクトリを作ります。
以降すべてそこで作業します。

$ mkdir helloworld
$ cd helloworld

sbtの設定

sbtを設定してScala.jsプラグインを使えるようにします。
まずproject/plugins.sbtに以下を書きます。

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

そしてbuild.sbtは以下のようにします。

enablePlugins(ScalaJSPlugin)

name := "Scala.js Tutorial"

scalaVersion := "2.11.7"

project/build.propertiesでsbtのバージョンを設定します。

sbt.version=0.13.8

sbtの設定は以上です。

Scalaの実装

src/main/scala/tutorial/webapp/TutorialApp.scalaを以下のようにします。
Scalaなので当然ですが、IntelliJ IDEAを使ったらちゃんとコードの補完ができました。

package tutorial.webapp

import scala.scalajs.js.JSApp

object TutorialApp extends JSApp {
  def main(): Unit = {
    println("Hello world!")
  }
}

これをsbtで「run」とすればHello worldと表示されます。

$ sbt
> run
[info] Compiling 1 Scala source to Scalajs/helloworld/target/scala-2.11/classes...
[info] 'compiler-interface' not yet compiled for Scala 2.11.7. Compiling...
[info]   Compilation completed in 12.775 s
[info] Running tutorial.webapp.TutorialApp
Hello world!

これだけみると単にScalaがコンパイルされて実行されただけに見えますが、
sbtの「last」を使うと、

> last
...
[info] Running tutorial.webapp.TutorialApp
[debug] with JSEnv of type class org.scalajs.jsenv.rhino.RhinoJSEnv

と実行にRhino(ライノー、JDKに含まれるJavaScript実行エンジン)が使われているので、
ちゃんとJavaScriptに変換されて実行されています。
ただJavaScriptファイルは生成されていないようでした。

Node.jsを使うように設定すると実行速度が早くなるそうです。
build.sbtに以下を追記します。

scalaJSStage in Global := FastOptStage

HTMLと一緒に使う

JavaScriptの生成

ScalaのコードをJavaScriptにコンパイルします。
sbtでfastOptJSとするとJavaScriptファイルが生成されます。

> fastOptJS
[info] Fast optimizing Scalajs/helloworld/target/scala-2.11/scala-js-tutorial-fastopt.js
[success] Total time: 2 s

これでtarget/scala-2.11/scala-js-tutorial-fastopt.jsができました。
生成されたJavaScriptのサイズは90Kバイトほどありました。
実際やっていることはconsole.log("Hello world!");だけなのにかなり大きくなってしまいます。
でも最初のころは(Scalaの標準ライブラリが丸ごと含まれていたため)Hello worldが16Mバイトもあったのと比べると
とても小さくなっています。

生成されたJavaScriptは以下のようなものでした。

(function(){
'use strict';
/* Scala.js runtime support
 * Copyright 2013 LAMP/EPFL
 * Author: Sébastien Doeraene
 */
 ()
$c_Ltutorial_webapp_TutorialApp$.prototype.$$js$exported$meth$main__O = (function() {
  var this$2 = $m_s_Console$();
  var this$3 = this$2.outVar$2;
  $as_Ljava_io_PrintStream(this$3.tl$1.get__O()).println__O__V("Hello world!")
});
 ()
$c_sjs_js_JavaScriptException.prototype.$classData = $d_sjs_js_JavaScriptException;
}).call(this);

あまり正しい比較ではないかもしれませんが、同じようなコードをCoffeeScriptで書けば、

class TutorialApp
  main: () ->
    console.log 'Hello world!'

で生成されたJavaScriptは

var TutorialApp;

TutorialApp = (function() {
  function TutorialApp() {}

  TutorialApp.prototype.main = function() {
    return console.log('Hello world!');
  };

  return TutorialApp;

})();

サイズは200バイトほどでした。
サイズが小さいだけでなく生成されたJavaScriptも人が読んで理解できるものになっています。

HTMLの作成

上で生成されたJavaScriptをHTMLのscriptタグで読み込み使ってみます。
scalajs-tutorial-fastopt.htmlを以下の内容で作成します。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>The Scala.js Tutorial</title>
  </head>
  <body>
    <script type="text/javascript" src="./target/scala-2.11/scala-js-tutorial-fastopt.js"></script>
    <script type="text/javascript">
      tutorial.webapp.TutorialApp().main();
    </script>
  </body>
</html>

これをブラウザで開き開発者メニューからJavaScriptコンソールを開けば、「Hello world!」と出力されています。

コンソールに出力する

jQueryを使う

上の例ではJavaScriptコンソールに出力されるだけでした。
次にjQueryを使ってHTMLを操作してみます。
実際にはHTMLを操作するのにjQueryは必須ではありません。
Scala.jsチュートリアルにはjQueryを使わないコード例もありますが、
そんなことする人いないと思うので省略します。

Scala.jsからjQueryを使うためのライブラリが用意されています。
これを使うとjQueryの関数がどういった型なのかが宣言されています。
build.sbtにライブラリを追加します。

libraryDependencies += "be.doeraene" %%% "scalajs-jquery" % "0.8.0"

「%%%」と%が3つ並んでいますが、これは間違いではありません。
「%%%」と書くと通常のScalaライブラリではなくScala.jsのライブラリという意味になります。

build.sbtに追加したら、

> reload
> ~fastOptJS
1. Waiting for source changes... (press enter to interrupt)

とします。「~」をつけるとファイルの変更を監視して自動的にコンパイルしてくれます。

Scala

先ほど作ったTutorialApp.scalaを以下のようにします。

package tutorial.webapp

import scala.scalajs.js.JSApp
import org.scalajs.jquery.jQuery

object TutorialApp extends JSApp {
  def main(): Unit = {
    jQuery("body").append("<p>Hello world!</p>")
  }
}

jQueryライブラリで関数が宣言されているので、関数名などの補完ができます。

依存するJavaScriptライブラリの追加

HTMLに依存するjQueryを追加する必要があります。

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>

とHTMLに書いてもいいですが、依存ライブラリが増えると面倒です。
Scala.jsは依存するJavaScriptライブラリを一つにまとめてくれる機能があります。
それを使うにはbuild.sbtに以下を追記します。

skip in packageJSDependencies := false

追記したら、

> reload
> ~fastOptJS

とします。これでtarget/scala-2.11/scala-js-tutorial-jsdeps.jsができました。
HTMLではこれを読み込みます。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>The Scala.js Tutorial</title>
  </head>
  <body>
    <script type="text/javascript" src="./target/scala-2.11/scala-js-tutorial-jsdeps.js"></script>
    <script type="text/javascript" src="./target/scala-2.11/scala-js-tutorial-fastopt.js"></script>
    <script type="text/javascript">
      tutorial.webapp.TutorialApp().main();
    </script>
  </body>
</html>

これでブラウザでscalajs-tutorial-fastopt.htmlを開けば「Hello world!」と表示されます。

ブラウザでHelloWorld

イベントを扱う

最後にHTMLのボタンをクリックされたイベントを扱う例を紹介します。

HTMLは以下のようにします。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>The Scala.js Tutorial</title>
  </head>
  <body>
    <button id="click-me-button" type="button">Click me!</button>
    <script type="text/javascript" src="./target/scala-2.11/scala-js-tutorial-jsdeps.js"></script>
    <script type="text/javascript" src="./target/scala-2.11/scala-js-tutorial-fastopt.js"></script>
    <script type="text/javascript">
      tutorial.webapp.TutorialApp().main();
    </script>
  </body>
</html>

次にScalaで、jQueryを使ってボタンをクリックされたらメッセージを表示します。

import scala.scalajs.js.JSApp
import org.scalajs.jquery.jQuery

object TutorialApp extends JSApp {
  // ボタンがクリックされたときのイベントハンドラーです。
  def addClickedMessage(): Unit = {
    // bodyにYou clicked...という要素を追加します。
    jQuery("body").append("<p>You clicked the button!</p>")
  }

  def setupUI(): Unit = {
    // HTMLのIDがclick-me-buttonの要素(ボタン)に対して
    // クリックイベントハンドラーを登録します。
    jQuery("#click-me-button").click(addClickedMessage _)

    // bodyにHello worldを追加します。
    jQuery("body").append("<p>Hello world!</p>")
  }

  def main(): Unit = {
    // jQuery()に関数を渡すとページの準備が完了した時点で、
    // 引数の関数を呼んでくれます。
    jQuery(setupUI _)
  }
}

setupUI _のようにメソッド名の後ろにアンダーバー「_」が付いているのは、
メソッドを呼んでいるのではないです。
list.foreach(println _)みたいなのではなく、eta-expansionというのらしいです。

scala> def f(): Int = 1
f: ()Int

scala> f _
res0: () => Int = <function0>

こんな感じに変換しているらしいです。
詳しいことはscalaのeta-expansionってなんなのかようやくわかったなどを参考にしてください。私は分かりません。

これをブラウザで開くと最初に「Hello world!」と表示されていて、
ボタンを押すたびに「You clicked the button!」というメッセージが追加されていきます。

jQuery

まとめ

初めのころはHello worldだけで10Mバイト以上あったのが数10Kバイトまで小さくなって、
実用的なサイズになったかな、とは思います。

Scalaで書けることのメリットの一つは型安全にできることだと思いますが、
型安全にやりたいだけならTypeScript使ったらいいと思います。

Scala.jsにはjQueryはライブラリが用意されていましたが、用意されていなかったら自分で関数の型など調べて
ライブラリ用意しないといけないようです。
TypeScriptはDefinitelyTypedでみんなの作った型宣言が簡単に入手できますが、
Scala.jsの場合はそうはいかないようです。ScalaとJavaScriptで使える型が違うからしょうがないことですが。
一応TypeScriptの型宣言からScala.js用に変換はできるらしいです。
scala-js-ts-importer参照。試してはいないです。

サーバ側がScalaでクライアント側とコードを共通にしたい、といったいうときに使うのがいいんでしょうが、
そもそもサーバ側とクライアント側で共通のコードってそんなにあるのかなあ、という気がしています。

そういうわけでまだイロモノの域にあるような気がしています。