LoginSignup
18
13

More than 5 years have passed since last update.

Scala.jsをnodeプロジェクトで扱う

Last updated at Posted at 2015-05-30

Scala.jsをnodeプロジェクトで扱おうとして挫折した話です。

モチベーション

  • 表現力の高いScalaで書ける
  • JS特有の処理(特にDOMとかHTML5系API)を別言語で書くのは地雷臭が半端ない
  • 変化の早いJSフレームワークにScalaでついていくは無理だと思う
  • 依存の少ないモデルやアルゴリズム系だけをScalaで書ければ生産性高そう
  • サーバーサイドもScalaで書けばisomorphic実現出来る気がする

本音を言えば誰もやってないから、取り敢えず地雷踏んでみるかというモチベーションです。

プロジェクト方針

基本的にnodeベースのプロジェクト構成を目指します。
これはnode関係のエコシステムが揃っていること、TSやbabelのようにaltJSで違う言語を使いつつエコシステムに乗せるノウハウがあるということでこういう方針にしています。

あと、個人的にあまりsbtが好きじゃないので、そちらに寄せたくないという気持ちもあります。

scala、sbtの準備

Scala.jsを実行するのにscalaとsbtが必要なため準備します。(環境を作ってから気づきましたがsbtだけ入れれば扱えるようです)
Macであればbrew install scala sbtでまとめてインストールすることができますが、それでは他環境(特にCI環境)でも同様に事前にインストールしなければいけなくなります。
そこで、bin-wrapperを使ってnodeプロジェクトにscalaとsbtを取り込みます。

で、作ったのがこちらになります。

npm install --save-dev scala-bin sbt-bin

npm経由でscalaとsbtをインストールすることで、npmのscriptでコマンドを実行できるようになります。

余談ですがScala.jsはスタンドアローン版が配布されているため、sbtを使わずに単独で実行することができるようです。

こっちで作ろうと思いましたが上手く実行できなかったで諦めました。
まぁIntelliJやScala IDEのようにIDEでScalaを書くときはsbtを使った方が扱いやすいので構いませんが。

Scala.jsの実行準備

Scala.jsのドキュメントを参考に設定とコードを書きます。

project/plugins.sbt
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.3")
build.sbt
enablePlugins(ScalaJSPlugin)

scalaVersion := "2.11.6"
src/main/scala/Hello.scala
import scala.scalajs.js
import js.annotation.JSExport

@JSExport
class Hello {
  @JSExport
  def world = {
    println("Hello World!")
  }
}

こんな感じScala.JSを追加して、有効にして、コードを書けば準備完了です。
コードと設定が上手くできているか確認するには

package.json
"scripts": {
  "compile": "sbt compile"
}

と追加して

npm run compile

とすることで確認ができます。
問題がある場合はエラーが出るのでメッセージをよく読んで格闘してください。

Scala.jsの実行

ここまで出来たらあとはScala.jsでScalaコードをJavascriptのコードに変換するだけです。

package.json
"scripts": {
  "build": "sbt fastOptJS"
}

と追加して

npm run build

と実行すればJavascriptのコードがtarget/scala-2.11/project-name-fastopt.jsに出力されます。
.mapも出力されるので、出力したコードを扱う場合はセットで扱うと良いと思います。

今回はfastOptJSを使って出力を行いましたが、Scala.jsではfullOptJSを使うことでminifyされたコードを出力することができます。
速度に差があるので、開発中なのかプロダクション用の出力なのかで切り替えてください。
今回は実用まで持っていけなかったので諦めましたが、gulpにこのあたりの処理を乗せて引数で切り替えたりすると良さそうです。

nodeライブラリとして扱えるようにする

Scala.jsで出力したJavascriptコードは

(function(){
// ...
}).call(this);

という形でブラウザで実行されることが前提の形になります。
persistLauncher := trueと設定を追加して、js.JSAppを継承したメイン関数をを作るとproject-name-launcher.jsが出力されて読み込み時に即実行を行えますが、nodeで扱う場合にはrequireでScalaで書いたクラスを取れないし、global汚染が発生するので使い難いです。
そこで、出力されたJavascriptコードをラップしてnodeで扱いやすい形にします。

index.js
'use strict';

var fs = require('fs');
var glob = require('glob');
var vm = require('vm');

var files = glob.sync('./target/*/*opt.js');
if (files.length === 0) {
  throw new Error('Not found script.');
}
var code = fs.readFileSync(files[0]);
var context = vm.createContext({
  global: global,
  console: console
});
vm.runInContext(code, context);

module.exports = context.Hello;

詰め切ってないので、もう少し良い書き方がありそうですが、取り敢えずはsandboxを作って実行し、そこからglobalに展開された作成したクラスをmodule.exportsで返します。
こうすることで他からのコードからでも呼びやすくなります。
(複数のクラスを扱うことを考えるとsbtプラグイン作ってnode用のlauncherを出力するのが良さそうですが、そこに行く前に挫折しました)

合わせてpackage.jsonもnpmで配布できるように設定を変更します。

package.json
"main": "index.js",

あとは.npmignorepackage.jsonfilesフィールドを使って、index.jsと生成したJavascriptコードのみをnpmにpublishできるようにします。
(このあたりも挫折してたどり着けなかったので細かい設定がないです)

テスト

Scala.jsを使う場合はScala定番のScalaTestを使うことができません。
(sbtベースで使おうとするとテストケースを発見することができません。IntelliJなどのIDE経由だと動かせます)

Scala.jsのトップページのTesting frameworksでどれもあまり聞かないフレームワークであまり食指が動きません。
今回の場合だと、どうせ変換したJavascriptコードに対してテストを書く必要があるし、あまり聞かないものを使って苦労するぐらいならmocha+power-assert構成でいいやという感じです。
鉄板構成すぎてこれといって構成が思いつかないときは、この構成にしておけば安パイですね。

テスト方法としては先ほど作ったラッパーに対して行います。
基本的にScalaでコードを書くときはイミュータブルにするので、他の補助ツールはいらない印象です。
(これも挫折して辿り着けませんでしたが非同期処理系は何かツールが必要になるかもです)

挫折した話

ちょっと長い前振りも終わってここからが本題です。
取り敢えず、構成も一通りできたので、実際にコードを書いてみよう!ということでscala-quizのMyListでも移植してみたところ見事に挫折しました。

applyを使えない

Scalaでapplyを定義するとMyList(1, 2, 3)と呼び出すことができます。
https://github.com/k-kinzal/scala-js-example-app/blob/master/src/main/scala/MyList.scala#L191-L204
https://github.com/k-kinzal/scala-js-example-app/blob/master/src/test/scala/MyListSpec.scala#L8

しかし、Scala.jsで変換したコードを扱う場合にMyList.apply(1, 2, 3)というようにapplyを明示的に呼び出さないといけません。
https://github.com/k-kinzal/scala-js-example-app/blob/master/test/MyList.spec.js#L9

地味に面倒臭いです。

objectをインスタンス化する必要がある

Scalaでobjectキーワードを使うとシングルトンのオブジェクトを作成することができます。
https://github.com/k-kinzal/scala-js-example-app/blob/master/src/main/scala/MyList.scala#L191-L204

しかし、Scala.jsで変換したコードを扱う場合に一度newしてインスタンス化してあげる必要があります。
https://github.com/k-kinzal/scala-js-example-app/blob/master/index.js#L18

地味に面倒臭いです。

プレースホルダがない

https://github.com/k-kinzal/scala-js-example-app/blob/master/src/test/scala/MyListSpec.scala#L15
https://github.com/k-kinzal/scala-js-example-app/blob/master/test/MyList.spec.js#L18-L20

ScalaとJSコードを見比べると分かるのですが、JSではプレースホルダがないので関数を渡す関数がダルいです。
JSからScalaを呼ぶとかしているので、こう感じてしまうのは仕方ないことなのですが、他にも細かくScalaの良い書き方ができないのでMPをガリガリ削られます。

Scala.jsは別に悪くない。

オーバーロードしたメソッドを出力できない

オーバーロードしたメソッドに@JSExportで出力を指定するとエラーが発生してビルドに失敗します。
https://github.com/k-kinzal/scala-js-example-app/blob/master/src/main/scala/MyList.scala#L164-L172

[error] /Users/Kinzal/Dropbox/Projects/scala-js-example-app/src/main/scala/MyList.scala:186: Exported method mkString conflicts with MyCons.$js$exported$prop$mkString

対応のしようがないので、オーバーロードしないか、1つのメソッドだけに@JSExportを指定するしかないです。

Scala.jsで変換したコードに無名関数を渡せない

https://github.com/k-kinzal/scala-js-example-app/blob/master/src/main/scala/MyList.scala#L17-L21
https://github.com/k-kinzal/scala-js-example-app/blob/master/test/MyList.spec.js#L18-L20

Scala.jsで変換したコードに無名関数を渡そうとするとTypeErrorが発生します。

{ 's$1': 'An undefined behavior was detected: function (a, b) {\n                    return a + b;\n                } is not an instance of scala.Function2',
  'e$1': 
   { 's$1': 'function (a, b) {\n                    return a + b;\n                } is not an instance of scala.Function2',
     'e$1': null,
     'stackTrace$1': null,
     stackdata: [TypeError: undefined is not a function] },
  'stackTrace$1': null,
  stackdata: [TypeError: undefined is not a function] }

is not an instance of scala.Function2と言われている通り、たぶんScala.jsで準備された関数しか渡せないんだろうなと思います。
どうやっても渡せなかったのでここで心が折れました。

おわりに

JS側に公開されるインターフェースを意識して、上手くやればnodeとScalaを共存させてあげることはできそうな雰囲気はあります。
ただ、自分はもう心が折れてもうダメです。

残骸を置いておくので、誰か自分の代わりにMyListのテストをmochaで全て通してください・・・。
たぶん、ここが全て通せるようになればTSやbabelを使うときにScala.jsを使うという選択肢を取れるようになってくると思います。


Scala関西 Summit 2015にScala.jsでLTに応募しようと思ったけどScala.js難しすぎた。

追記(2015/05/31)

foldLeftで無名関数渡せない・・・と思ってたけど、まだやりようがありそうな予感です。

18
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
13