Scalaのfor文を使いこなすには

今回はおまけです。scalaのfor文を中心として、その周りにあるトピックを扱いたいと思います。

  • 開発に使えるトピック
  • while文はfor文とどう違うか
  • Eitherの謎イディオムを読解する

つまるところ、for文に調べていたら一緒にわかったことをつらつらとまとめます。もともと前回の記事に盛り込んでいたのですが、あまりにも長くなったので分割することになりました。

注意:文中の scala> で書き始めているコードはREPL環境で実行しています。

コンパイラの内部処理を覗き見する

for文が複雑になってくると、実際にどのように実行されているか分かりづらくなります。仕様書に従って手作業で変換をしてみるのは学習としてやるのはいいですが、開発中にデバッグの一環としてやるものではありません。どのオブジェクトのメソッドが呼び出されているのか正確に知りたくても、複雑なコード相手では、なかなか手作業で変換するのには大変です。人間がわざわざやることじゃないです。そうした時に便利なのが -Xprintオプションです。 scalacmanコマンドで調べると次のようになります。

フェーズの数が多く、どれが何を示すのか把握しきれてないところもありますが、筆者がよく使うのは parser, typer, jvmの三つです。

  • parserは文法上の処理のフェーズです。for文の糖衣構文が変換されるのはこのフェーズになります。
  • typerは型推論のフェーズです。暗黙の型変換の解決もこのフェーズで行われます。
  • jvmはJVMのバイトコードの生成のフェーズです。バイトコードを直接読むのは無理なので、最終的な結果の直前にどのような状態だったかを見ることができます。

allは簡単な実行文で一度見てみると挙動が見て取れて面白いです。膨大なログをみることになるので、時間があるときにやってみるといいでしょう。

-Xprintを利用しよう

scalaコマンドでも -Xprintを利用することはできます。REPL環境を -Xprint:parserで立ち上げてみるとしましょう。

起動時の処理のコンパイル状況も出ますが、そこは無視してOKです。試しに簡単なfor文を実行してみます。

while文はどうなるのか

for文の実体についてをここまで見てきたのですが、ここでwhile文がどう処理されるかをみて、for文と比較してみましょう。
REPL環境で -Xprint:parserに設定して実行してみます。

見慣れない表記になっていますが、while文は再帰関数に変換されます。上の例では while$1が再帰的に呼ばれているのがなんとなくわかると思います。このwhile文の内部的な変換はScalaの文法としては正しいものではないので、コピーしてソースファイルに持ってきても動きません。
演算子の記号は内部的なメソッド名に置き換えが行われています。 <$lessに、 +=$plus$eqに置き換わっています。この置き換えられたメソッド名も普通に使うことは可能です(わざわざ使うものではありません)。
他の言語であれば再帰関数の呼び出し回数が多い為スタックオーバーフローでも起こしそうな実装ですが、末尾再帰になるのでScalaの場合は特に問題なく実行可能です。興味があればどうして問題がないか調べてみると面白いです。
for文の場合はメソッドの呼び出しに変換でしたが、while文は再帰関数になります。完全に似て非なるものであると理解しておきましょう。使い所もかなり異なります。do-while文もwhile文と同様に再帰関数になります。

Tuple22が限界の問題はどうなるのか

積み残している話題を消化していきましょう。for文中に値の定義があった場合タプルに変換されるということを第二回で解説しましたが、タプルは Tuple22までしかありません。ジェネレータと値の定義が22個付随していたらどうなるのかもちろん気になりますね。やってみましょう。

使われる機会はほぼないとは思いますが。 Tuple2[Tuple22, Any]に変換されます。前から順にジェネレータと値の定義が22こ処理され、残った値の定義についてもルール通りに処理されていると考えるとこの挙動は理解できます。値の定義が増えると Tuple3[Tuple22, Any, Any], Tuple4[Tuple22, Any, Any, Any]と増えていき、値の定義が43こになると Tuple2[Tuple22[Tuple22, Any, ... Any], Any]になります。
仕様書上の解釈として間違いではない挙動ですが、こうなるとは新鮮な驚きがあります(が、知っていたところで役立ちません)。

Eitherの利用

知っていたところで役に立つのか微妙な話を二つ続けてしましたが、役立つ話をしていきましょう。
Eitherクラスとfor文の組み合わせには、定石的な書き方があります。

process1から process5Left[Exception]もしくは Right[Result]を返す処理です。 resには上から processXを実行して Right[Result]を返した最初の結果が返ってきます。
このような書き方をイディオムとして知っている人は多いのではないかと思います。理解できているかと聞かれると自信がない人が多いのではないでしょうか。for文が繰り返し処理だと考えていると全く理解できないのではないかと思います。
こんなときこそ -Xprint:parserをつかいましょう。 process1Resultも定義していないので、実行はできませんが、パース結果は見ることができます。

process1.left.flatMapといったメソッドが呼び出されることになります。 Left, RightflatMap, mapの組み合わせでどのような結果になるかを見てみましょう。

Left("a").leftであれば flatMapmapによる変換が動くが、 Right("a").leftでは、変換が動いていないことがわかります。
Eitherのメソッド leftrightは左右を限定して、 flatMapmapを使えるようにする機能を提供しています。そのためこのような挙動になります。
process1.leftであれば、 process1Right型の値を返した場合、 .left.flatMapは何の影響もあたえず、 process1の実行結果をそのまま保持します。逆に Left型の値を返した場合、 .left.flatMapによって値が変換されます。
これで、先のイディオムで最初に Rightを返した結果が得られる理由はわかったのではないかと思います。for文を眺めてもなかなか理解できるものではありません。実際にどのように実行されるかを把握することで初めて理解できます。for文の実体がメソッドの呼び出しであることはこのように影響してきます。

本当に理解できているかチェックしよう

基本的なことを調べる、分かった気になってないか確かめる意味でfor文を追っかけてきましたが、Scalaのfor文は思想的に今までやってきた言語と違うものでした。全然違う世界です。新しい言語を勉強するってこういうことですよね。同じことができるだけなら元の言語だけで十分です。Scala面白いよ。