Scala.jsでD3.jsを使う
Scala.jsの概要
Scala.js http://www.scala-js.org/ という、ScalaからJavaScriptのコードを生成できるシステムがあり、ちょっといじって見ているが、かなり良く出来ているという印象を持った。IntelliJの補完も使って、型安全+Scalaの抽象化能力を最大限に活用できる。
http://tototoshi.github.io/slides/sendagaya-js-scala-js/#1
このスライドで書かれているように、初出は去年なのだが、進歩が猛烈なスピード。現在ver. 0.5.1。この調子で行けば、じきに高機能AltJSの代表格となるのではないだろうか。
使ってみて感じたメリット。
- Scalaの言語仕様のほぼ全機能(リフレクション以外)を使える。
- 型安全で、Scalaのパワフルな抽象化が使える。JSのオブジェクトも型付けできる。
- 当然IntelliJで補完も効く。
- JavaScriptとの接続(Scala.js←→JSの双方向)が明快、便利。
- これまで使った感触だと、あまりハマリポイントはない模様。
- ソースマップも生成される。
- 最大最適化後のファイルサイズは150KB程度まで小さくなった(最小のプロジェクトで)。
- 手持ちのMac Mini (Late 2012, Core i7 2.3GHz)でビルド時間は10秒ほど。
- 最適化を弱めると1MBのJSファイルが出来た。ビルド時間は3秒。
- 導入が簡単。
- Scalaプロジェクトの.sbtファイルに2行書き足すだけですぐに使える。
- スタンドアロンのコンパイラも最近用意された。
- HTMLからの呼び出しは、生成された単一のJSファイルを読み込むだけ。
- DOM操作やjQueryのバインディングも用意されている。
- Scalaプロジェクトの.sbtファイルに2行書き足すだけですぐに使える。
Scala.jsの導入
@mizchiさんの記事(http://mizchi.hatenablog.com/entry/2014/02/14/211400)でも書かれているように、まず短いサンプルとしてサンプルのレポジトリをクローンして走らせてみるのが良い。
git clone --depth 1 https://github.com/lihaoyi/workbench-example-app cd workbench-example-app sbt gen-idea sbt ~fastOptJS
これでIntelliJでスラスラ書きながら、変更を検出して自動コンパイルできる。
チュートリアルは短くて分かりやすい。 http://www.scala-js.org/doc/tutorial.html
言語はScalaそのものなので、Scalaの言語以外に追加で必要な知識はJSとのFFI(言語間接続)だけで、それさえわかればすぐに使える。
FFIのポイント
Scala.js→外部のJS
- JSのオブジェクトは
js.Object
トレイトを継承したトレイトとしてScala上にマッピングされる。
外部のJS→Scala.js
js.Array(2,3,4,5)
というようにしてJSネイティブの配列を作れる。js.Dynamic.literal("name" -> "John", "age" -> 19)
というようにしてJSネイティブのオブジェクトを作れる。- js.FunctionNはscala.FunctionNと等価。コールバックに用いる。
Scala.js→D3.jsのFFI
型付きのFFIを作成したいわけだが、TypeScript用に書かれたD3.jsの型定義があるので、それを元に以下のコンバータを使ってScala.js用の定義を作成してやる。このレポジトリには他にも多数のライブラリがある。
D3.d.ts: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/d3
Importer from TypeScript type definitions to Scala.js: https://github.com/sjrd/scala-js-ts-importer
コンバータが吐き出したScalaファイルは大方ちゃんと出来ているようにみえるが、実際走らせようとすると、コンパイルエラーと実行時エラーがあり、いくつか修正する必要があった。 以下に生成されたもの、手動修正後のソース、そしてその2つのdiffを示しておく。すべてを手で修正する時間と気力はなかったので、とりあえずサンプルコードが動くようになるところまでの最低限の修正をした(後ほど完全なバージョンを作って公開したい)。
Importerが生成したソース: https://gist.github.com/nebuta/7beb3d6e8d17a7ba5142
修正後のソース: https://gist.github.com/nebuta/c20e0db85bf3e7a0dab1
Diff: https://gist.github.com/nebuta/c703bc0317822c253752
修正のポイント:
override
を追加する必要があるメソッドがいくつかある。(IntelliJ上で表示されるので分かる)- D3のSelectionオブジェクトはJSの配列を拡張しているので、
js.Array
をextendsする。 js.Any
型の引数のコールバックを使う関数(例えばattr()など)は、以下のようにsubtypeも含めたシグネチャに変える必要がある。
def attr[A <: Any](name: js.String, valueFunction: scala.Function2[A, js.Number, Any]): Selection = ???
js.Array[A]
は(どういうわけか)invariantで定義されている、配列を取る関数はdef func[A <: HogeType](x: A)
としてやる。そうしないと、型が合わないことが頻発する。- Scalaの仕様上、オーバーロードしている関数で、複数の関数にデフォルト変数を設定することができない。適宜
= ???
を削除する。
使用例
作成したバインディングを用いて、D3.jsの公式サイトにある例をScala.jsに書き換えたものを以下に挙げる。
デモ: http://nebuta.github.io/d3js-scala/
ソースコード: https://github.com/nebuta/ScalaJS-D3-Example/tree/master/src/main/scala/example
まとめ・感想
- ウェブアプリのサーバーとクライアントを同じ言語で書きたいというのはわりと根強くある希望だと思うが、例えばScalaでPlay frameworkをサーバーに使い、Scala.jsをクライアントに使う、というふうにすれば、かなり生産性が高くなりそう。
- AltJSとJSとの相互運用は、双方の変数・関数がどのようにマッピングされるかというセマンティクスの接続(?、そういう言葉があるか知らないが)を理解するのが概して難しい。
- AltJSの中だけでなるべく閉じたような構造にすれば制御しやすくなる。
- 違う言語を接続するのは本質的にチャレンジングな課題。CoffeeScriptやTypeScriptのような「薄い」ラッパーであれば比較的単純になるが。
- 最近の大型JSフレームワーク(AngularJS, Ember, Meteorなど)との組み合わせも、どのくらいスムーズに統合できるのか、興味がある。
- エラーを取り除くために、バインディングのコードにアドホックともいえる修正を結構したので、まだ安心感がない...。(FFI一般にそうだが、正しく書いていないと不可解な実行時エラーが生じうる。)
余談だが、D3.jsについての感想:
- D3.jsは公式サイトに載っている実例の鮮やかさも手伝ってか、人気が非常に高い。しかし、いくつか改良点も考えられる。
- コンセプトを理解するのがわりと難しい。
- メソッドチェインが長く、何が操作対象なのかが理解しづらい時がある。
- このことが、型付き言語でD3を使いたくなる動機である。
- 以前作った、HaskellからD3のコードを生成するライブラリ。複雑なJSライブラリへの型付けの実験でもある。http://hackage.haskell.org/package/d3js
- D3.js自体が抽象化レイヤーであるためか、他の抽象化と相性が悪いような気がする。
- JSフレームワークとD3.jsを組み合わせようと思いいろいろ調べたのだが、両方ともデータ/ビューバインディングを実現するライブラリであるため、統合がスムーズにできない感が強い。
- 典型的には、JSフレームワークによる画面更新が終わった時に、データバインドをオフにしたようなDOM要素にD3を使って描画する、という継ぎ接ぎ的な構造になる。
ということで、強力なAltJSが出てきている今、そろそろD3.jsに影響された、型付きのAltJSを使ってより高度に構造化した次世代可視化ライブラリが出てきても良い頃かと思われる。Scala.jsやPureScript、あるいはHasteを使って一から作るのもいいかもしれない。
参考資料
他にも役に立ちそうなスライド http://www.slideshare.net/kamekoopa/kyonkekkonlt