こういうツイートを見かけた。
Scala3 って for-yield じゃなくて for-do なの???
— りと (@rito_528) 2024年1月8日
言われてみれば、たまにScala 3のコードサンプルでそういうの見たことあるな~と思った。ので調べたメモ。
tl;dr
- 値を返したいときは
yieldを書く e.g.for x <- xs yield x * 2 - 副作用だけが目的なら
doを書く- ジェネレータのカッコを明に書くなら
doは省略できる e.g.for (x <- xs) println(x) - ジェネレータのカッコを省略するなら
doを明に書かなければならない e.g.for x <- xs do println(x)
- ジェネレータのカッコを明に書くなら
用語
- ジェネレータ
- Scalaの
forにおいて、for x <- xs do println(x)のうちx <- xsの箇所のこと。
- Scalaの
The code
i <- intsis referred to as a generator.
forループ
doを使うパターンでは、こういう感じで書かれている:
for x <- 1 to 100 do println(x) // 1 // 2 // 3 // ...
一方、Scalaのfor式といえばyieldを使うのがおなじみだ:
for x <- 1 to 100 yield x * 2 // => Seq(2, 4, 6, 8, ...
で、実際のところどうなのか調べてみたところ、キーとなるのは値を利用するかどうかであった。
ループではdoを書くように?
公式ドキュメントのControl Structures | Scala 3 — Book | Scala Documentationによれば、forループを行うときはfor ... do ...を利用せよ、とある。
Scala 2では以下のように書けていたコードが……
for (i <- ints) println(i)
Scala 3では以下のように書く必要がある(カッコは任意なので、カッコ付けてもカッコ付けなくてもよい):
for i <- ints do println(i)
Scala 2系から3への差分は以下の通りだ:
- ジェネレータを包むカッコは省略できる
- ループさせる箇所に
doを書く必要がある
カッコ付けてdoを書かないか、またはカッコ付けずにdoを書くか
コンパイラのドキュメントによれば、ジェネレータ部分がカッコで包まれているときはdoを書かなくても良いようだ。
ForExpr ::= ‘for’ ‘(’ Enumerators0 ‘)’ {nl} [‘do‘ | ‘yield’] Expr ForYield(enums, expr) / ForDo(enums, expr)
| ‘for’ ‘{’ Enumerators0 ‘}’ {nl} [‘do‘ | ‘yield’] Expr
| ‘for’ Enumerators0 (‘do‘ | ‘yield’) Expr
言い換えると、カッコを書かない場合はdoを書かなければならない。値を再利用したい場合はカッコと無関係に常にyieldを書く。
yield
さて、これだけだとyieldの出番が無い。yieldはいつ使うのか?その答えは値を利用するかどうかにかかっている。
前述のコードをもう一度持ってこよう。
for i <- ints do println(i)
このコードの返り値はUnitだ。
% scala-cli scala> val x = for i <- ints do println(i) // ... scala> :t x Unit
旧来のScalaユーザにとってはお馴染だが、ループした結果の値を再利用したい場合(そちらのほうが多いだろう)には、doではなくyieldを利用する:
// 再掲 for x <- 1 to 100 yield x * 2 // => Seq(2, 4, 6, 8, ...
こう書くと、ループごとにx * 2の結果が集められ、最終的にSeq[Int]の値が返される。
まとめ
ループだけが目的の場合は記法のバリエーションがあることがわかった。といっても、Scalaでforを扱う場合、値を集めてくる(== yieldを使う)ことが多いと思う(たいていの文脈でfor式*と呼称されるのはそのためだ)。
自分も普段yieldしか使わないので、このような書き方があることを知らなかった。
カッコを使うか使わないかだけれど、正直好みの問題だと思う。自分は伝統的にカッコを使ってきたけど、こだわる必要もないと思う。今度からはカッコを外してみようかな。