scala/for式入門
ここで学ぶこと
他の言語と同じように、forはループさせるために使う
が、「ループ」が指せる範囲が他の言語よりも少し広い
Scalaのfor式は他の言語のforよりもできることが多い、という認識を持てればよい
Scalaのforの違い
forが式である
多重forを1つの構文で書ける
意外となんでもループできる
forは式である
Scalaでは、他の多くの言語と同じようなfor構文を用意しています
code:scala
for (i <- 0 until 9) {
// ^^^^^^^^^^^^^ ここをenumeratorと呼ぶ
// ここはbodyとか本体とか呼ぶ
println(i)
}
enumerator
(変数 <- 0 until 9)の部分のこと
body
enumeratorの次に来る箇所
他の多くの言語では、forは文の一つであって、式ではありません
code:javascript
// js
let str = '';
for (let i = 0; i < 9; i++) {
str = str + i;
} // for自体は値を返さない
Scalaではforは式であり、for自体が値を返せます
式である = 値を返せる、ということ
code:scala
for (i <- 0 until 9) yield i
// => Seq(0, 1, 2, 3, 4, 5, 6, 7, 8)
Scalaのfor式では、値を返すためにyieldを使います
これはまた次の節で説明します
forで値を返すには
for式で値を返させるには、yieldキーワードを使います
yieldしないと、for式の値はUnitになります
yieldキーワードは1つの式を受け取ります
yield式の中ではenumeratorで登場した変数を利用できます
この例ではiがそう
code:scala
for (i <- 0 until 9) yield i * 2 // => Seq(0, 2, ... 14, 16)
多重for
for式が多重になる場合、enumeratorをくっつけて1つのfor式にすることができます
code:scala
// この式は
for (i <- 0 to 10) {
for (j <- 0 to 10) {
println(s"$i * $j = ${i * j}")
}
}
// この式と等価
for (i <- 0 to 10; j <- 0 to 10) {
println(s"$i * $j = ${i * j}")
}
// enumeratorは()の中で;で区切る代わりに、{}の中で改行で区切ることもできる(こちらが一般的)
for {
i <- 0 to 10
j <- 0 to 10
} {
println(s"$i * $j = ${i * j}")
}
// 単一の式はそのまま書ける
for {
i <- 0 to 10
j <- 0 to 10
} println(s"$i * $j = ${i * j}")
もちろん、多重forでもyieldできます
code:scala
for {
i <- 'A' to 'Z'
j <- 1 to 5
} yield s"$i$j" // => Seq(A1, A2, A3, A4, A5, B1, B2, B3, B4, B5, C1, C2, C3, ...)
多重forでyieldすると、1つのコレクションにまとめられます
enumeratorの種類
enumerator(日本語でどう書くか定まってなさそうなのでそのまま書いてる)には、値を生成するジェネレータと、それをフィルタするフィルタ、ただの定義の2種類があります
code:scala
for {
i <- 1 to 10 // ジェネレータ
d = 2 // 定義
if i % d == 0 // フィルタ
} yield i // => Seq(2, 4, 6, 8, 10)
これらは基本的にどの順序で書いてもよい(書いた順に発動される)です
が、定義を先頭に書くことは今のところできません
普通にforの外に書けばよいため
これらを組み合わせて、柔軟なループと値の生成を実現できます
練習問題
for式を使って、1から50までの整数でFizzBuzzを実装してみる
code:scala
for {
n <- 1 to 50
} yield n match {
case n if n % 6 == 0 => "fizzbuzz"
case n if n % 2 == 0 => "fizz"
case n if n % 3 == 0 => "buzz"
case n => s"$n"
}
脱線 〜なんでもforで書けるよ編〜
ここまで読んだ時点で、たいていのループ処理は書ける
ここでおトクな知識を紹介します
map
いったんmapの話をします
Scalaにはmapというコレクションメソッドがあり、同じくループを行うことができるようになっています
code:scala
(0 to 5).map(i => i * 2) // => Seq(0, 2, 4, 6, 8, 10)
同じことをforでやると:
code:scala
for (i <- 0 to 5) yield i * 2
なるほど〜
filter
次に、filterの話をさせてください
Scalaにはfilterというコレクションメソッドがあり、コレクションを一定の条件で抽出できるようになっています
code:scala
(0 to 5).filter(i => i % 3 == 0).map(i => i * 2) //=> Seq(0, 6)
同じことをforでやると:
code:scala
for {
i <- 0 to 5
if i % 3 == 0
} yield i * 2 // => Seq(0, 6)
なるほど〜
flatMap
最後に、flatMapの話をさせてください
flatMap = map して flatten する
(おさらい) map = コレクションをループして全体に関数を適用し、その結果で新たにコレクションを作る
「全体に関数を適用し、その結果で新たにコレクションを作る」ことをインテリ用語で写像するとか射影すると言います
flatten = 二重になってるリストを潰す
code:scala
List(List(1, 2), List(3), List(4, 5)).flatten // => List(1, 2, 3, 4, 5)
何が便利かというと:
mapでは個々の要素に関数を適用する以上のことはできません
コレクションの構造には口を出せない
flatMapはコレクションを返す関数を受け取るので、より多くのことができます
それだけじゃ直感的に分からないので、具体的に見てみよう
code:scala
val xs = List(1, 2, 3, 4, 5)
// mapはコレクションの中身を操作するが、形を変えられない
xs.map(x => List(x, x)) // => List(List(1, 1), List(2, 2), ..., List(5, 5))
// flatMapは要素を潰して消したり、
xs.flatMap { x => x match {
case x if x % 2 == 0 => List()
case x => List(x)
}} // => List(1, 3, 5)
// 要素を増やしたりもできる
xs.flatMap(x => List(x, x + 1)) // => List(1, 2, 2, 3, 3, 4, 4, 5, 5, 6)
flatMapはコレクションの壁を壊せるので強力な道具です
実は、これもforで書ける
code:scala
val xs = List(1, 2, 3, 4, 5)
for {
x <- xs
x2 <- List(x, x + 1)
} yield x2 // => List(1, 2, 2, 3, 3, 4, 4, 5, 5, 6)
世界が隠した真実
実はforでmapやfilter、flatMapと同等のことができるのは当然で、
というのもforはmapやfilter、flatMapのシュガーシンタックス、つまり多少変わった書き方であって、内部的には全くmapやfilter、flatMapに変換されてしまっているからなんです
な、なんだってー
言い換えると、forでできることは大抵map/filter/flatMapでやれる
真実の検証
Scalaコンパイラのフェーズはscala-cli -Xshow-phasesしたら出てくる
この中で、for式の展開はtyperフェーズで実施されている(っぽい)
特定フェーズである式がどのように展開されたかを確認するにはscala-cli -Vprint:フェーズする
code:scala
% scala-cli -Vprint:typer
scala> for {
| x <- List(1,2,3,4,5)
| x2 <- List(x, x + 1)
| } yield x2
syntax trees at end of typer // rs$line$3
package <empty> {
final lazy module val rs$line$3: rs$line$3 = new rs$line$3()
final module class rs$line$3() extends Object() { this: rs$line$3.type =>
{
def $anonfun(x: Int): IterableOnceInt = {
def $anonfun(x2: Int): Int = x2
closure($anonfun)
}
)
closure($anonfun)
}
)
}
}
val res0: ListInt = List(1, 2, 2, 3, 3, 4, 4, 5, 5, 6) 展開された式がゾロゾロ出てくるけど、要するにflatMapしてその中でmapしている、という式に変換されていることがわかる