yieldの付くfor式

中村学氏(以下、中村):次はfor expression、for式についてです。「式」と言うように、Scalaのforは式です。他の言語ではfor文と言われますが、Scalaのforは式です。そのため評価すると値になります。Scalaの式は大きく分けて2種類あります。予約語のyieldが付くものと、付かないものです。

yieldが付かないfor式は、基本的に他の言語のforeach構文とほぼ一緒です。全体の式としての評価結果はユニットという、何の意味ももたない値になります。yieldが付かないfor式の意味合いは、for文や他の言語のforeach文とほとんど一緒です。ここではyieldの付くfor式について、中心に見ていきたいと思います。

comprehension=for内包表記

yieldの付くfor式をfor comprehensionと呼びます。聞いたことがある方いますか? サンプルコードがありますが、こんな感じでforとyieldを組み合わせて使います。このcomprehensionは、日本語で言うと“for内包表記”と言われたりします。

内包表記とは何かというと、内包表記の反対語として外延的記法、外延表記という表記の仕方があります。それは何かというと、集合を表現するときに、その要素の具体的な値を列挙して書いていくようなかたち。{ }を使ってカンマ区切りで具体的な値を列挙していって、その集合全体を表現するようなものを外延的記法と呼びます。

外延的記法は具体的でわかりやすいですが、無限の要素がある、例えば整数や自然数など、無限の要素がある集合を表現しようとすると、列挙しきれないなどいろいろな問題があるので、省略のようなことが使われます。そうした場合、誤解なく集合全体を表現するのが難しい場合があります。そのため、それを避けるために、内包的記法、内包表記と言われるような書き方が存在しています。

これは具体的に要素の条件を明示して、要素の集合を表すような書き方です。例えばxは整数かつx÷2の剰余は0になるというものの、x全体。それが偶数の集合になる、といったような数学の書き方があります。これを内包表記と言ったりします。

一部のプログラミング言語では、この内包表記を言語としてサポートしています。PythonやHaskellとか使われている方はご存じかもしれませんが、リスト内包表記と言って、リストの値を構築するのに、内包表記のような感じで書けます。

このx in rangeやy in rangeのようなものを書くことで、xとかyの条件を明示して、x * yでその集合全体の構成要素を表します。Haskellも、xが1から3までの間と、yが5から7までの間と。x * yが要素の構成要素なので、集合を表すような書き方ができます。数学の書き方と似ていますね。

Scalaのfor-yieldも内包表記

Scalaのfor-yieldも、実はこれと同じもので、内包表記になります。Scalaのforを書いて、yieldを付けたものが他の言語で書くのとまったく同じ意味合いを持っている感じです。その要素の一個一個を表すx * yが一番左側に来るのか、それともfor式全体のyieldの後ろにあるのかで、大きく見た目が違いますが、やっている意味合いとしては同じです。

数学の書き方的なものは、そのxの要素が一番条件の左側に来るので、PythonやHaskellなどは数学に親しんでいる人はわかりやすいかと思います。余談ですが、僕なんかはけっこうxとかyとかの条件が先に左側に来て、最後にそれを使ってyield式が来るというので、個人的にはこちらのほうが好きです。

そのforの( )の中ですね。こちらにxとかyとかの条件を書いて、yieldが各要素の実際の計算になります。Scalaのfor式は( )の代わりに{ }を使えます。この{ }を使うと何がうれしいのかというと、複数の条件があったときはだいたいセミコロンで区切りますが、{ }で書くと、改行をセミコロンの代わりにできます。

条件式がxとyの2つぐらいであればそんなに変わりませんが、これが4つ、5つぐらいになると、1行で書くのはなかなかわかりづらくなっていきます。そのため、この{ }を使って改行で表すというのがよく使われます。条件が多くなってくると、こちらの{ }のほうが見やすくなります。ここまでが内包表記の一般的な説明です。

その他のプログラミング言語と内包表記

今までリストの作り方を内包表記でみてきましたが、Pythonだとリスト内包表記以外にも、生成したいオブジェクトごとに内包表記が用意されています。

例えばリスト内包表記だとさっきの[ ]ですが、セット内包表記の場合はこの{ }を使うと、全体としてセットになります。その要素の条件式や、その要素の式自体は同じですが、作られるオブジェクトがリストなのかセットなのかで分かれ、構文が用意されています。

Haskellでもリスト内包表記がありますが、HaskellにはGHCという実行環境があり、GHCには内包表記の拡張が用意されていて、その拡張を使うと、リスト以外の要素でも内包表記を使って書けます。例えば、Maybeのようなものを作りたいときに、内包表記を使えます。Scalaの場合、実はけっこうHaskellの内包表記と近いですが、基本的にmapとflatMapというメソッドを持っているオブジェクトなら、全部このfor式、内包表記で書けます。

最初にxの条件式にSeq("1", "2", "abc", "4")のようなものを用意して、yの右辺は全体的にOptionになる感じです。yのTry(x.toInt).toOptionとやると、1とか2とかはIntにできますが、abcがIntにできないので、ここがNoneになります。

yield式でyの2乗をやっていると、1と2と4の2乗としてSeq(1, 4, 16)が計算でき、モナドになってなくてもmapかflatMapを持っていれば、何でもfor式として使えるような構造になっています。なぜそんなことになっているかというと、あくまでfor-yieldは糖衣構文、シンタックスシュガーとして作られています。

for-yieldで書かれた式が、コンパイル時にはmapとflatMapを使った式に変換される感じです。(視聴者の「最初do式の互換だと思ってたら混乱しました」というコメントを受けて)僕も最初はdo式の互換だと思っていましたが、実はdo式よりはモナド内包表記なんです。HaskellのreturnとScalaのyieldが対応しているのかと思っていたら、実はそんなことなくて混乱していたんですが。

Scalaのfor式はHaskellのdo構文だよ、という説明をたまに見かけますが、逆に混乱を生む場合もあります。導入されたモチベーションはHaskellのdo式と似ていますが、for構文としてはdo構文よりも内包表記のほうが意味合いとしては近しいものだと覚えてもらえると、たぶんいいかなという気がしています。

シンタックスシュガー(糖衣構文)でmapかflatMapに変換される話ですが、上のように書いたfor-yieldの式は下のように変換されます。最初のSeq("1", "2", "abc", "4")の部分がflatMapのメソッド呼び出しになって、x =>が付いてさらにtryのtoOptionが付いて、そこにmapの呼び出しになる。

一番最後のところでy * yになるような感じで変換されます。今、二重のネストなのでflatMapとmapになっていますが、基本的に二重、三重、四重になってくると、flatMap、flatMap、flatMapとなって、最後のyield部分がmapに近しい値になる、mapに変換されるような構造になります。

for-yieldの結果は最初の型を見れば解決する

for-yieldの結果がどんな型になるかは、最初のジェネレータ、最初の<ーの右側の型を見ると判明します。ここもSeqのflatMapになるので、全体としてはSeqになるだろうな、という感じです。

最初がVectorであれば、VectorのflatMapになるので、for式全体もVectorになりますよ。逆に、最初のx <- の右側がリストなので、リストのflatMapになり、for式全体がリストになります。こういった感じでfor式全体の型を解釈できます。

たいていはそこを見れば解決しますが、最初の部分で決定できない場合がたまにあります。例えばMapです。Mapの場合、最後のyield式がタプルになっていれば、Mapに変換できますが、タプルになっていないとMapをflatMapにしても、Mapにできないので、Iterableになるケースがあるんですね。

そのため、Mapが使われていると、yield部分を見ればfor式全体の型が何なのかがわかるかな、という感じです。

for-yieldの要点まとめ

まとめにいくと、for-yieldはScalaにおける内包表記です。ゆるふわなので、mapとflatMapをもっていれば全部コンパイラは通します。最初の部分の型がわかれば、だいたい結果の型がわかります。

(視聴者の「ポリモフィズムでIterable解決している?」というコメントを受けて)そうですね。MapのflatMapメソッドが戻り値の型をMapにするのかIterableにするのかは、けっこう難しいメカニズムを使っていて。渡されるファンクションの型によって結果の型が変わるので、それはけっこうScalaの型システムのおもしろポイントですね。また2.12と2.13でこの辺の実装がちょっと変わっていたりするので、興味のある方は深追いしてみるといいかもしれません。

(次回につづく)