こちらは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が出る問題とか、タブ補完が効かない問題など、色々と雑な感じにはなっています。。。
そのあたりのフォローはまた後日に(‘・ω・`)