for文がわからない

Web業界でfor文がわからないなどと言ってしまうと、プログラミング初心者として扱われそうだが、Scalaはそうじゃないと声を大にして言いたい。C系プログラミング言語のfor文とScalaのfor文は同列で語ることは難しいのです。

人生の半分以上をプログラミングと名のつくものと過ごしてきた上で言うのは憚られるが、私にはScalaのfor文が難しい。この記事を書くためにいろいろと調べてまわるといかに分かったつもりだったかが思い知らされました。

本連載は全3回を予定しています。全体を通してfor文のデバッグができるようになることを目標に掲げています。本記事は簡単なfor文の動きを見ます。次回は仕様書をベースにfor文の文法ルールを見ていきます。第三回はコンパイル時の動きを中心に見ていく予定です。PHPやJavaScrptなどのWebで使われる言語と違いを含めて、Scalaのfor文を徹底解剖して理解を深めることにしましょう。

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

JavaScriptでfor文をwhile文に書き換えてみる

Scalaと互換性のあるJavaや、Webでよく使われるPHPやRuby、JavaScriptなどでは、基本的にfor文とwhile文は相互に書き換えが可能です。もちろん、綺麗なコードになるかは別ですが、同じ処理、同じロジックは実装可能です。continueの振る舞いが違うから微妙に違うとも言えますが、これは多少無理をすれば実装できなくありません。ざっくり言ってしまうとfor文とwhile文はループ処理をする似た者同士というのが常識でしょう。

JavaScriptを例にとると以下の二つは同じ処理です。

for文で書いた方がシンプルでいいぐらいの違いです。

「ループ処理はwhile文でも事足りるけど便利でよく使うからfor文も用意してある。」

これが私が今まで持っていた常識です。この常識をScalaの学習に持ち込むと OptionFutureをfor文と一緒に使う場面で面食らうことになります(なりました)。Scalaではfor文とwhile文は全く別物です。(whileの話題は第3回にします)

簡単なforループの例

下のような、ごく簡単なforループの実行を実行してみよう。

Scalaを触ったことがない人でも結果が予測できるような簡単なforループです。結果は1から5までが一行ずつ表示されます。

ここで1から5ではなく、1だけにしたくなり、以下のように直したとします。

これはコンパイルエラーが出て動きません。(素直に代入すればいいというのは脇に置いておきましょう)

Intforeachが定義されていないというエラーが出ます。
このエラーは文法エラーではありません。あくまでも foreachが見つからないというエラーになります。 toがforの構文の一部であれば文法エラーが出て欲しいところです。forループなのになぜ foreachの有無が問われるかパッとは理解できません。

foreachの出処

foreachがエラー文に出てくるのかを色々調べてみると、forループはそれぞれ下のような式に変換され実行されるからのようです。

これらは同じものとして変換されます。間違った例も同様に変換されます。

1Int型なので foreachがないため実行できないということです。
Scalaの言語仕様では、for文というものはそのまま実行されることはありません。自動で変換される対象であり、あくまでも人間が書きやすくするために用意されている代替構文にすぎません。上の例ではforループの実体は foreachメソッドを呼び出す処理になります。
このように、コンパイル前に自動でコードが変換されるルールを”シンタックスシュガー”(糖衣構文)と呼びます。Scalaにはfor文以外にもシンタックスシュガーが多くあり、知らないとごく簡単なコードを書くにも難儀することになります。

1 to 5 はなぜ実行できるのか

ここで生まれる疑問は 1 to 5foreachはあるのかということです。
foreachが実装されているということはおそらくオブジェクトなので、オブジェクトの型を調べてみましょう。

scala.collection.immutable.Rangeでした。 Rangeには foreachが実装されています。

では、なぜ 1 to 5Rangeになるのでしょうか? この問いの答えとして「toは演算子で、2つの整数からRangeを作る機能を持っている」は50点ぐらいの回答です。
正しく答えようとするとかなりの周りに道になります。Scalaではすべての演算子がメソッドの呼び出しのシンタックスシュガーです。下の処理は等しくなります。

むしろ内部的には右辺の形式で実行されます。ここで 1の型である Inttoメソッドが実装されていれば、説明は終わるのですが、 Intには toというメソッドはありません。(Intの実装ソース)

toメソッドはどこから来たのか

Intの実装には toというメソッドは見つかりませんが、Intのリファレンスページには toはあります。さて、この toはどこから出てきたのでしょうか。リファレンスをよく読むと

Implicit information
This member is added by an implicit conversion from Int to RichInt performed by method intWrapper in scala.LowPriorityImplicits.

とありますね。 intWrapperというメソッドで Intから RichIntに暗黙の型変換が行われていると書いてあります。 LowPriorityImplicitsはオブジェクト名でリファレンスを探しても見つかりません。
github上のScalaプロジェクトのソースを全文検索するとこのソースに出てきます。 scala.Predefの親クラスが scala.LowPriorityImplicitsです。 scala.Predefはスコープに自動でimportされるものだそうです。当たり前のように使っている printlnなども定義されています。(こんなところにあったのかということに一緒に驚きましょう わぉ)

つまりforループはこう動いている

今回題材にしてきた下のforループは次のようなステップで動いています。

ステップ1) forループを foreachに変換する

ステップ2) toは演算子として評価されるので、 toをメソッドに変換する

ステップ3) 1の型である Intには toメソッドはないので、 toメソッドを持つ型への暗黙の型変換を探索する

ステップ4) 暗黙の型変換を適用する

元の文とは全く違うものになりましたね。ごく簡単なfor文にこれだけ複雑な処理が走っています。
これでやっと簡単なforループがどのように動いているかが解体できました。forループひとつ動かすにもいくつもの暗黙のルールが動いています。

まとめ

  • Scalaのfor文はあくまでも人間が書きやすくするために用意されている代替構文にすぎない
  • forループの実体は foreachなどのメソッドの呼び出しである
  • toはforの構文の一部ではなく演算子(=メソッド)として処理される
  • scala.Predefというスコープが自動でimportされている
  • scala.Predefには IntRichIntにする暗黙の型変換メソッドが定義されている

次回予告

次回はもっと複雑なfor文の変換のルールを読み解きます。