S-JIS[2011-10-01/2012-09-02] 変更履歴

ScalaのREPLで他のアプリを動かす実験

ScalaREPL上でHadoopのHDFSを操作してみた。


動機

PigHadoopのHDFS上のファイルを操作していて、ファイル名を扱うのにローカル変数が使えなかったりして不便に思った。
で、ScalaREPLも対話型ツールなので、この上でHadoopのHDFSを操作することが出来るんじゃないか?と思って試してみた。

REPL上でHadoopのクラスをインポートすれば、そのクラスのメソッドを呼び出すだけなので、何でも出来るはず。
REPL(scalaコマンド)起動時にクラスパスを指定できるので、Hadoopのライブラリーを指定すればいけるはず。
しかしHadoopはコンパイルするだけなら2つくらいのjarファイルを指定すればいいが、実行するとなるとたくさんjarファイルを指定しないといけなくなる。
しかもWindowsではCygwinを使って起動することになるので、そのパスをどうするか…なんて考えたくもない(爆)

一方、Hadoopの起動はhadoopシェルで、中はjavaコマンドを起動しているだけなので、こちらもクラスパスを追加できる。
しかも起動するプログラム(クラス)を指定することも出来る。
Scalaのライブラリー(jarファイル)をクラスパスに追加し、REPLのメインクラスを実行すれば動くんじゃなかろうか。


クラスパスの指定

hadoopシェルの場合、環境変数HADOOP_CLASSPATHにクラスパスを入れておくと、実行時に参照してくれる。
そこで、ScalaのライブラリーをHADOOP_CLASSPATHに入れる。

export HADOOP_CLASSPATH="$SCALA_HOME/lib/*"

JDK1.6では、クラスパスのファイル名部分に「*」があると その位置の全jarファイルを読み込んでくれるので、自分で展開する必要は無い。

ただし、Cygwinの場合はSCALA_HOMEはWindowsのパス形式だろうから、UNIX形式に直しておく必要がある。

export HADOOP_CLASSPATH=$(cygpath -u "$SCALA_HOME/lib/*")

ちなみに、「*」入りのパスを確認しようと思ってechoを使うと、シェルの機能によって各ファイル(スペース区切り)に展開されてしまう。環境変数の中(値)は「*」のままなので、勘違いしないように。ってゆーかずっと「上手くいかないなー」って悩んでたよorz
ダブルクォーテーションで囲むと「*」のまま表示される。

$ echo $HADOOP_CLASSPATH
/cygdrive/c/scala/scala-2.9.1.final/lib/jline.jar /cygdrive/c/scala/scala-2.9.1.〜

$ echo "$HADOOP_CLASSPATH"
/cygdrive/c/scala/scala-2.9.1.final/lib/*

実行中のシェルがCygwinかどうかを判別する方法


REPLの実行

ScalaのREPLは、scala.tools.nsc.MainGenericRunnerというオブジェクトから実行される。
したがって、hadoopシェルの引数にそれを書けばいい。

$ hadoop scala.tools.nsc.MainGenericRunner
Welcome to Scala version 2.9.1.final (Java HotSpot(TM) Client VM, Java 1.6.0_27).
Type in expressions to have them evaluated.
Type :help for more information.

scala>
Failed to initialize compiler: object scala not found.
** Note that as of 2.8 scala does not assume use of the java classpath.
** For the old behavior pass -usejavacp to scala, or if using a Settings
** object programatically, settings.usejavacp.value = true.

と思ったら、なんかエラーが出た。

どうも、通常のscalaコマンドの場合、Scalaのライブラリー(jarファイル)はブートストラップのような形で読み込んでいるが、
hadoopシェルでは通常のjarファイルと同じように読み込まれているので、問題があるらしい。
エラーメッセージに書かれている通り、settings.usejavacp.valueというシステムプロパティーにtrueを指定すれば解決する。


Hadoopの場合は環境変数HADOOP_OPTSにオプションを指定することが出来る。

$ export HADOOP_OPTS=-Dscala.usejavacp=true

$ hadoop scala.tools.nsc.MainGenericRunner
Welcome to Scala version 2.9.1.final (Java HotSpot(TM) Client VM, Java 1.6.0_27).
Type in expressions to have them evaluated.
Type :help for more information.
scala> import org.apache.hadoop.conf._
import org.apache.hadoop.conf._

scala> import org.apache.hadoop.fs._
import org.apache.hadoop.fs._

scala> val conf = new Configuration
conf: org.apache.hadoop.conf.Configuration = Configuration: core-default.xml, core-site.xml

scala> val fs = FileSystem.getLocal(conf)
fs: org.apache.hadoop.fs.LocalFileSystem = org.apache.hadoop.fs.LocalFileSystem@1a15597

scala> fs.listStatus(new Path("/temp")).map(_.getPath)
res1: Array[org.apache.hadoop.fs.Path] = Array(file:/temp/asakusa, file:/temp/scala)

上手くいった!^^


scalaコマンドに渡す引数(-cp等)は、scala.tools.nsc.MainGenericRunnerの引数として指定する。

$ hadoop scala.tools.nsc.MainGenericRunner -cp mylibrary.jar

scala -i スクリプト

scalaコマンドでは、「-i」オプションを付けると、REPL起動時にスクリプトファイルを読み込んで実行してくれる。
インポート文なんかを書いたファイルを用意しておけば便利だろう。

$ scala -i スクリプト

が、Scala2.9.1では「-i」オプションにバグがあり、使うと固まる。(SI-4945。Scala2.9.0.1までは大丈夫だったらしい)
こればっかりは直るのを待つしか仕方ない…。
一応、REPLコマンドに「:load」というのがあって、スクリプトファイルを読み込んで実行できる。ちょっと手間だが、これで我慢。

SI-4945は2012-08-07にFIXされた(1年くらいかかってるよorz)ので、たぶんScala2.10からは大丈夫になると思われる。[2012-09-02]
Scala2.9.1およびScala2.9.2では直っていないが、「-Yrepl-sync」を付ければ実行できるようだ。

$ scala -Yrepl-sync -i スクリプト

実行後のリセット

さて、REPLを終わるときは「:q」を入力する。
Cygwinだとすんなり終了できて問題ないが、通常のUNIX(CentOSのGnomeターミナル)では、終了後のキー入力がおかしくなる。(打ったキーが表示されないし、改行できない(見えないだけで、コマンドは実行できる))

こういうとき(コンソールが文字化けしている時など)は、resetコマンドを試すのが常套手段。
今回もresetを実行すると正常に戻った。

ちなみにCygwinにはresetは無いので、実行しようとしたらエラーになる。


Scala実験へ戻る / Scala目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま