こういうツイートを見かけた。
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 <- ints
is 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
しか使わないので、このような書き方があることを知らなかった。
カッコを使うか使わないかだけれど、正直好みの問題だと思う。自分は伝統的にカッコを使ってきたけど、こだわる必要もないと思う。今度からはカッコを外してみようかな。