こちらはScala AdventCalendar 2014の7日目の記事です。
今日はScalaのカスタムREPLの作り方についての話。なお今回は(Scala REPL同様)StandaloneなREPLアプリの作成を目的としているので、:power
モードは主眼ではありません。
モチベーション
ScalaのREPLは手元のローカルマシンでAPIを試してみたいときや、ちょっとした計算をしたいときにはとても便利なのですが、やや凝ったことをしたいときなど、そのまま使うには不便さを感じることがあります。
具体的にはAndroidのAPIをScalaのREPLから叩けるようにしたかったのですが、Android環境をJVMでエミュレートするためにはカスタムClassLoaderを使ってAndroid APIのClassを書き換える必要があって。。。という感じ。
これはちょっと特殊なモチベーションかもしれませんが、クラスター上などの特定の環境で実行させたいとき(e.g.spark-shell
)、自作ライブラリのsandbox環境を提供するにあたって特定の場面でよく使うコマンドを追加したい、など思われる方はいるかもしれません。
そんなとき、意外とScala REPLを拡張する記事を書いている人が少なかったので、今回はScalaのソースコードを読みながらカスタムClassLoaderを使用する方法、コマンドを追加する方法について書くことにしました。
参考:Create your custom Scala REPL … 数少ないREPL拡張方法に関する記事。
成果物
1 2 3 4 5 6 7 8 9 10 11 |
|
ClassLoaderの差し替え(Classのロード時にクラス名をprint)と、myCommandというコマンドの追加をしています。
REPLとは
Read–Eval–Print Loopの略で、対話型の開発環境。ユーザーの入力したコードを一行から評価する。
ScalaのREPLはscala
コマンドから開始できる。
ScalaのREPLの構成
scala.tools.nsc.interpreter
パッケージがREPLに相当。インタプリタのソースコードがそれなりの量あるので一見大変そうだが、構成自体はシンプル。REPLをカスタマイズする上で最低限見る必要がある場所に絞って、以下では解説します。
エントリポイント
scalaのスクリプトの中身を見ると、以下の通りscala.tools.nsc.MainGenericRunner
のmain
関数をエントリポイントとしてREPLを起動していることがわかる。
1 2 3 4 5 6 7 8 9 10 |
|
Main関数の中でLoopを呼び出し
scala.tools.nsc.MainGenericRunner
のmain
関数の処理は、同コンパニオンクラスのprocess
関数に移譲している。この関数の中で、ILoop
のprocess
関数を呼んでおり、このクラスがREPLのLoopに相当。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
参考:scala.tools.nsc.MainGenericRunner.run
Loopの中でインタプリタを呼び出し
scala.tools.nsc.interpreter.ILoop
はREPLのLoopに相当するクラス。コマンドの判定や、入力したコードのインタプリタへの移譲などを行っている。
REPLのコマンド(e.g.:help
)の定義はstandardCommands
変数。overrideしたcommands
関数において追加することでカスタムCommandを実装できる。
また、ILoop
のcreateInterpreter
関数内で、インタプリタのメンバ変数var intp:IMain
を初期化している。
なおインタプリタ intp
の実装クラスはIMain
を継承したILoopInterpreter
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
|
インタプリタの中でClassLoaderを生成
scala.tools.nsc.interpreter.IMain
がインタプリタの主たるコード。
private var _classLoader:ClassLoader
がClassLoaderのメンバ変数だが、privateなのでclassLoader関数をoverrideしてカスタムClassLoaderを生成する。
ただし、IMain
のclassLoader
関数の戻り値の型はscala.reflect.internal.util.AbstractFileClassLoader
になので、カスタムClassLoaderもこれを継承させる必要がある点に注意。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
カスタムREPLを作る
基本的には解説した以下のクラスに相当するものを継承なり自前で作るなりすれば、カスタムREPLが作れます。
scala.tools.nsc.MainGenericRunner
… REPLを起動するmain関数、Loopの呼び出しscala.tools.nsc.interpreter.ILoop
… Loopの実装。Commandの判定。インタプリタへ処理の移譲。scala.tools.nsc.interpreter.IMain
を継承したscala.tools.nsc.interpreter.ILoop.ILoopInterpreter
… インタプリタの実装。ClassLoaderの生成。scala.reflect.internal.util.AbstractFileClassLoader
を継承したカスタムClassLoader … インタプリタに使用させる。
先ほども記載しましたが、完成物は以下の通り。試し方はREADME参照のこと。
REPLを拡張している実例
Apache Sparkのspark-shellは、REPL上で入力したコマンドをcluster上で実行させるために、REPLを拡張している。以下のファイル群がカスタムREPLに相当する。
- spark-shell
- org.apache.spark.repl.Main
- org.apache.spark.repl.SparkILoop
- org.apache.spark.repl.SparkIMain
ScalaMatsuriで発表されたScaliveもREPLを拡張してJVM環境を触れるようにしている。
余談:Scalaの:power モード
Scala REPLに備わっている:power
モードを使用すると、Scala REPLコードのpublicな変数・関数へのアクセスが可能で、varなら置き換えも可能。
例えば上記のカスタムClassLoader挿入は、:power
モードを利用してこんな風にも書けます。(ClassLoaderのサイズが膨らんでくるとつらさはありますが)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
ただその一方でREPLのコマンドの判別はILoopクラスで行わており、MainGenericRunnerの内部関数中で直接呼び出されているのでPowerModeからのカスタムCommandの追加は不可能なはず(とはいいつつ、何がしかのhackもありそうな気がするのでもしあればコメント欄で教えてください。)
余談
肝心の、オレオレClassLoaderを使ってAndroid APIを叩けるScala REPLアプリは間に合わず。現状でも、未解決のwarningが出る問題とか、タブ補完が効かない問題など、色々と雑な感じにはなっています。。。
そのあたりのフォローはまた後日に(‘・ω・`)