🧺

Scala 2 の yield をちゃんと理解する(REPLで動かす実例つき)

に公開

0. 準備:REPL を開く

scala

この記事は Scala 2.13 系 を想定。以降は REPL にコピペで動きます。
(出力例は resX: ... = ... の形になります)

1. まずは差を見る:for(副作用) vs for ... yield(結果を返す)

1-1. 副作用だけの for

scala> for (i <- 1 to 3) println(i)
1
2
3

→ 返り値は Unit(結果は返さない)

1-2. 値を返す for ... yield

scala> val r = for(i <- 1 to 3) yield i * 10
val r: IndexedSeq[Int] = Vector(10, 20, 30)

→ 各回の i * 10 を集めて 新しいコレクション(ここでは Vector)を返します。

2. yield の正体(デシュガリング:糖衣構文の展開)

覚え方はこれでOK:

  • for (x <- xs) yield f(x) ≒ xs.map(x => f(x))
  • for (x <- xs if p(x)) yield f(x) ≒ xs.withFilter(p).map(x => f(x))(filter でも近い)
  • for (x <- xs; y <- ys) yield g(x, y) ≒ xs.flatMap(x => ys.map(y => g(x,y)))
  • for (x <- xs) println(x) ≒ xs.foreach(x => println(x))

つまり yield は map / flatMap / withFilter の糖衣です。

3. 具体例いろいろ

3-1. 変換:要素を2倍にして返す(= map)

scala> val xs = List(1, 2, 3)
val xs: List[Int] = List(1, 2, 3)

scala> val doubled = for (x <- xs) yield x * 2
val doubled: List[Int] = List(2, 4, 6)

愚直版:

scala> xs.map(_ * 2)
val res1: List[Int] = List(2, 4, 6)

3-2. 抽出+変換:偶数だけ2倍(= withFilter/filter → map)

scala> val xs = List(1,2,3,4,5,6)
val xs: List[Int] = List(1, 2, 3, 4, 5, 6)

scala> val evens2x = for {
     |   x <- xs
     |   if x % 2 == 0
     | } yield x * 2
val evens2x: List[Int] = List(4, 8, 12)

愚直版:

scala> xs.filter(_ % 2 == 0).map(_ * 2)
val res2: List[Int] = List(4, 8, 12)

3-3. 3-3. 複数ジェネレータ:直積(= flatMap + map)

scala> val as = List("a", "b")
val as: List[String] = List(a, b)

scala> val bs = List(1, 2)
val bs: List[Int] = List(1, 2)

scala> val pairs = for {
     |   a <- as
     |   b <- bs
     | } yield s"$a$b"
val pairs: List[String] = List(a1, a2, b1, b2)

愚直版:

scala> as.flatMap(a => bs.map(b => s"$a$b"))
val res3: List[String] = List(a1, a2, b1, b2)

3-4. パターンで取り出す(Map からキーだけ)

scala> val m = Map("x" -> 1, "y" -> 2)
val m: scala.collection.immutable.Map[String,Int] = Map(x -> 1, y -> 2)

scala> val keys = for {(k, v) <- m} yield k
val keys: scala.collection.immutable.Iterable[String] = List(x, y)

愚直版:

scala> m.map { case (k, _) => k }
val res4: scala.collection.immutable.Iterable[String] = List(x, y)

3-5. Option をつなぐ(どちらか None なら None)

scala> def plus(a: Option[Int], b: Option[Int]): Option[Int] =
     |   for {
     |     x <- a
     |     y <- b
     |   } yield x + y
def plus(a: Option[Int], b: Option[Int]): Option[Int]

scala> plus(Some(2), Some(3)) // -> Some(5)
val res5: Option[Int] = Some(5)

scala> plus(Some(2), None) // -> None
val res6: Option[Int] = None

愚直版:

scala> def plus2(a: Option[Int], b: Option[Int]): Option[Int] =
     |   a.flatMap(x => b.map(y => x + y))
def plus2(a: Option[Int], b: Option[Int]): Option[Int]

scala> plus2(Some(2), Some(3)) // -> Some(5)
val res7: Option[Int] = Some(5)

scala> plus2(Some(2), None) // -> None
val res8: Option[Int] = None

4. yield の“返り型”のざっくりルール

  • 最初のジェネレータ(x <- xs の xs)がコレクションなら、だいたい同系統のコレクションが返る
    • List → List、Vector → Vector、Set → Set(※重複は落ちます)
  • Option なら Option が返る
  • Range は 2.13 では通常 Vector など IndexedSeq 相当が返る(実装詳細は気にしすぎない)

確認:

scala> (for (x <- List(1,2,3)) yield x).getClass.getName
val res14: String = scala.collection.immutable.$colon$colon

scala> (for (x <- Vector(1,2,3)) yield x).getClass.getName
val res15: String = scala.collection.immutable.Vector1

scala> (for (x <- Set(1,2,2)) yield x).getClass.getName
val res16: String = scala.collection.immutable.Set$Set2

(環境により実クラス名は多少違いますが、概ね上記の性質になります)

5. for の中で一時変数を定義 → yield

scala> val xs = List(1,2,3,4,5)
val xs: List[Int] = List(1, 2, 3, 4, 5)

scala> val result = for {
     |   x <- xs
     |   y = x * 3
     |   if y % 2 == 1
     | } yield y
val result: List[Int] = List(3, 9, 15)

愚直版:

scala> xs.map(x => x * 3).filter(_ % 2 == 1)
val res13: List[Int] = List(3, 9, 15)

6. よくある勘違い・落とし穴

  • Q. yield は関数の return と同じ?
    → 違います。 yield は「各イテレーションの式の結果を集める」ための合図。
    関数から抜ける return とは別物です。
  • Q. for に yield を付け忘れると?
    → Unit になってしまう(値が返らず副作用だけ)。
    「結果を得たいのに空っぽ…」というバグの定番。
  • Q. Set や Map に yield したら?
    → その性質(重複削除・キー一意)が効きます。
    例:Set(1,1,2).map(_*2) は Set(2,4)。
  • Q. 複数ジェネレータの順番は影響する?
    → 影響します。 for { a <- as; b <- bs } は as.flatMap(a => bs.map(...)) と同じ。
    先に外側 as が来ます。

7. まとめ(覚え方)

  • for ... yield は 結果を返す for。
    map/flatMap/withFilter の糖衣構文(読みやすさのための記法)。
  • 副作用のために回すだけなら yield なし(または foreach)。
  • Option など「つないで失敗を伝播」する処理も for で読みやすく書ける。

Discussion