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

第一回はScalaのFor文が制御構文ではなくメソッドを呼び出す代替構文であることを確認しました。第二回では複雑なfor文をどのようにメソッドに変換しているかを文法から読み解きました。
最終回の今回はメソッドだったらどうなるのかということを中心に今まで積み残してきた内容を扱います。他の言語とfor文とどう違うかとScalaのfor文のメリット・ディメリットを把握するのが今回の目的です。for文がどのような実体を持っているかはわかっていることを前提として話を進めますので、わからない人は第一回から読むのをお勧めします。

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

break文がない

for文の言語仕様書に記述がある話を中心にここまではしてきましたが、ここからは少し話を変えて今回は最初に仕様書に”ない”話をしたいと思います。

言語仕様書を読みあさっても、breakがありません。continueもありません。CとCの文法の影響が濃い言語ではbreakを制御構文の一部として定義されています。for文やwhile文のループから出たり、switch文から抜けるためにbreakは利用されますが、Scalaではfor文は糖衣構文ですし、switch文もありません。制御構文としてのbreakは存在しません。

Scalaのfor文は制御構文ではない

どんなプログラミング言語であれ、ファンクションや関数、メソッドといったサブルーチンとして実行されるものは呼び出し元に向けて制御を行うという発想はほとんどありません。Scalaのfor文の実体である foreachにしろ mapにしろ、引数で受け取った関数を呼び出しはしますが、逆に関数から foreachmapの挙動についてアクセスする方法はありません。そのため、for文を途中で止めるという発想自体がScalaにはないといえます。for文を呼び出したら必ず最後まで実行されることを前提に利用するしかありません。for文の実体がメソッドであるということはそういうことを意味します。

繰り返しになりますが、Scalaのfor文はメソッドの呼び出しです。制御構文ではありません。 foreachmapを使っているとしても、メソッドの内部でループさせてるから制御構文的な何かなのではと思うかもしれませんが、それも正しくありません。どのように実行されるかはあくまでもメソッドが属しているオブジェクトの実装に依存します。ループが実装されていなければ当然ループになりません。

では、Scalaのfor文をどのように理解するべきなのでしょうか。

Scalaにおけるfor文のイメージ

foreachmapともに、値を内包しているインスタンスの値それぞれに処理を行うメソッドです。 SeqRangeであれば、順番が定義されますが、 Setやコレクションの Mapといったものでは順番というものがありません。そのため、汎用的に使われる foreachmapには処理順についての一般則はありません。むしろ順番通り実行される SeqRangeの方が例外と言えます。
また、メソッドの引数の関数は無名関数で定義されるので、繰り返し実行される際のスコープは前後の実行状況に影響を受けません。外の名前空間をクロージャとして参照可能な以外は独立した存在になります。文字通りfor each(各々に対して)の意味通りの実行が行われます。メソッド mapは地図という意味ではなく、数学用語の写像のことです。集合の構成要素すべてに何かしらの変換をかける手続きのことを指します。どちらも、個別の要素に対してそれぞれ実行する点が共通しています。
Scalaのfor文はジェネレータに指定された全要素に対して行うことを前提とし、さらに処理順を問わず独立して実行されるというのが正しい理解になります。一般的なCライクのfor文とScalaのfor文を実行イメージを図にすると次のようになります。

Untitled-2

Scalaのfor文は処理自体が分裂して平行して動いているようなイメージです。並列で動いているので、breakもcontinueもありえないわけです。

手軽に並列処理

イメージ上、並列しているなら、そのまま並列処理にしてくれればいいのにと人は欲張りなもので考えます。
Scalaにはfor文を使って簡単に平行処理を実装する方法が用意されています。ちょっと使ってみることにしましょう。普通のfor文とマルチスレッドで実行されるfor文を書いてみましょう。

並列処理にした例の結果は実行するたびに違う結果になります。この例では少し並列処理が行われいるのかわかりづらいと思います。もう少し並列処理が目に見える処理を作ってみましょう。

Thread.sleepは指定したミリ秒だけ処理を止めるので、平行処理をしていない方は0.1秒から0.5秒までを足した1.5秒程度の実行時間で、平行処理をしていると同時に走れるスレッド数にもよりますが最短で0.5秒の実行時間になります。体感でも十分に速度の差がわかるのではないかと思います。結果の出力も平行して同時に動いている様子がよくわかると思います。
平行処理の例と平行処理でない例の差は .parの有無だけです。これで並列処理になります。
さて、なぜでしょう。
forループの実体は foreachだと前回まで説明してきました。今回もそれを当てはめると (1 to 5).par.foreach(...)というメソッドが呼ばれているはずです。 (1 to 5).parは何か調べてみましょう。

(1 to 5).parscala.collection.parallelというパッケージの Rangeに相当するクラスです。このパッケージは並列処理に特化したパッケージで、各種コレクションクラスの並列処理対応したものが用意されています。
くどいようですが、Scalaのfor文はメソッドの呼び出しであり、その実装は属するクラスに依存します。つまり、 foreachmapが平行処理として実装されているクラスさえあれば、for文はいとも簡単に平行処理になります。それを簡単に行ってくれるのがコレクションクラスの parというメソッドになります。
この平行処理ですが正しく使えば非常に強力です。使いこなすのは難しいですが、Scalaのfor文はこんな使い方もできるのだと知っておくといいでしょう。
このパラグラフで紹介したのは平行処理でしたが、クラスの実装次第でfor文の形をした何かを作り出すことが可能です。使い方の幅と柔軟性は非常に高いです(が、もちろん乱用はしない方がいいです)。

Scalaのfor文が向かない処理

他の言語であればfor文を使うのが妥当だが、Scalaではfor文に処理させるのに向かない処理があります。
一つ目が、途中で停止するべき処理です。breakがなく、最初から最後まで実行されることを前提とするためです。条件に一致した最初の一件を取得するような処理が該当します。JavaScriptであれば、普通にfor文を使う以下のような処理はScalaのfor文で作ると無駄が多い処理になります。

Scalaのfor文にはbreak文がないのでこのような打ち切り処理は作れません。
一般化ができる方法はありませんが、この場合は findを使うのが妥当です。

breakが使えないかわりにと言うと少し違うかもしれませんがScalaではこういったメソッドが充実しています。他の言語であればとりあえずfor文でやってしまえることも多いですが、Scalaではそうはいきません。

二つ目が、直前のループの結果を利用する処理です。つまるところ動的計画法には使いにくいです。動的計画法って何という人も多そうですが、有名なところではフィボナッチ数列のアルゴリズムです。

プログラミング経験者がScalaを勉強しようとしてfor文がよくわからなくなるのはこの辺じゃないかと思います。この場合は foldLeftfoldRightを使うのが妥当なシーンです。

Listの特性上、末尾の値を追加するより、先頭に値を追加する方が高速なのと、末尾の値を参照するより、先頭の値を参照する方が高速なので、以下の方がいい実装です。

for文で実装しようとするとかなり面倒もしくは、非常にきたない感じになるので出来ないぐらいに思っておいた方がいいと考えてます。

それでもbreakしたいあなたへ

Scalaのfor文は制御構文ではありません。そのためbreak文はありません。
そうは言ってもちょっとしたものを書くにもそんな制限を受けていたら堪らない。そんなにストイックにプログラミングをしていくのには無理があります。
Scalaのfor文を途中で抜けることはできるのでしょうか。答えは、できます。当然のように抜け道はあります。
制御構文としてのfor文が動かないなら他の制御構文を使えばいいじゃないという話になります。処理を途中で止めて抜けることのできる制御構文は2つあります。return文とthrow文です。

return文は、 defで定義されるメソッド内でのみ使えます。メソッド内で作られた無名関数でreturnを実行した場合、親となるメソッドのreturnになります。この特性を利用すればbreakに相当する挙動ができます。

上のfindを利用する例をfor文で描き直してみます。

このように書くことで、for文の途中でも処理を打ち止めることができます。

もう一つの方法のthrow文を使う方法です。

for文を止めるために正常であっても例外が発生していないのにthrowを投げるのには好ましい使い方とは言えません。こんな方法は本来紹介しない方がいいのかもしれませんが、Scalaの標準ライブラリ util.control.Breaks内の breakbreakableは内部でthrowしてcatchする構造になってます。これはいいのかは一概に言えませんが、標準ライブラリだけ例外として扱うのが妥当だと思います。( util.control.Breaksのソース)

これらのfor文を抜ける処理は並列処理が実装されたのオブジェクト相手では途中で止めることは出来ない注意が必要です。

return文を利用した場合は必ず最初の値になります。try-catchでは何が得られるかは安定しません。
並行処理を込みで考えてもと安定しているのはreturn文を利用する場合と言えます。使う制約はどちらにせよかなり大きいです。Scalaのfor文はできる限り途中で止めない方がいいでしょう。

continue文はあるか

for文が foreachmapに変換されるのであれば、引数になっている関数内でreturn文を使えばcontinueになるのではないかと思えます。実際にやってみましょう。

メソッドの外側でreturn文は使えないというエラーが出ます。これは、 foreachに変換した結果でも同じです。

この記事を書くために調べるまで誤解していたのですが、return文は無名関数(ラムダ関数)では利用できません。事実上クラスメソッド内でしかreturn文は使えません。無名関数に変換を行うfor文ではreturn文をcontinueの代わりにすることはできません。ならば、無名関数でなければ良いのです。

continueをするだけのためにメソッドを定義するのは大げさですが、なんとなくそれっぽい動きにはできます。

まとめ

  • Scalaのfor文は制御構文ではない
  • Scalaにはbreakがない。continueもない
  • Scalaのforには途中で止めるという発想が存在しない
  • for文がどのように実行されるかはオブジェクトの実装に依存する
  • それでもbreakしたいあなたには手段はあるにはある

最後に

もともとfor文を調べないといけないと思ったのはSlickでのクエリ作成でなぜかfor文を使えるという一見奇怪な挙動を見たことに始まります。
これは何かが違うと。
for文の実体がメソッドであることを理解していない筆者は何かが自分の知っているfor文と違うと感じたいのでいろいろ調べてみた結果が本稿になりました。これだけ調べてもなんでも分かっているとは思えないほど奥深いものがScalaのfor文にはあります。

長い上に回りくどく読みにくいところもあったかと思いますが、最後まで読んでいただきありがとうございました。

for文の周辺をまとめた記事をまとめとして後日公開します。