for式のforeach/flatMap(map)展開について

More than 1 year has passed since last update.


概要

for式が実際どのように展開されるのかわかった気になっていたけど、結局よくわかってなくて、ちゃんと調べたら理解できたので、自戒の念も込めて書いた記事です。 1


言語仕様

6.19 For Comprehensions and For Loopsに書かれていることは簡単で、



  • yieldないfor式は、foreach展開


  • yieldあるfor式は、flatMap/map展開

になります。これだけ。

つまり大雑把に言えば、


  • 単純に値を処理したいだけの時は、yieldのないfor式 (foreach展開)

  • 値をmap(型変換など)して返したい時は、yieldをつけたfor式 (flatMap/map展開)

を使用すればよいです。

ちなみに、for式中のifはwithFilterに変換されます。


foreach

yieldのないfor式

for (p <- e) expr

for (p1 <- e1; p2 <- e2) expr

for {
p1 <- e1
p2 <- e2
} expr

これらは以下と等価です。

e.foreach(p => expr)

e1.foreach(p1 => e2.foreach(p2 => expr))

2番目の例を改行して整形すると

e1.foreach(

p1 =>
e2.foreach(
p2 =>
expr
)
)

なるほど、nested loops。


flatMap/map

yieldがあるfor式

for (p <- e) yield expr

for (p1 <- e1; p2 <- e2) yield expr

for {
p1 <- e1
p2 <- e2
} yield expr

これらは以下と等価です。

e.map(p => expr)

// p1 <- e1 を展開

e1.flatMap(p1 => for { p2 <- e2 } yield expr)

// p1 <-e1 / p2 <- e2 の両方を展開
e1.flatMap(p1 => e2.map(p2 => expr))

つまり、このfor式は値を返します。

複数のジェネレータ(p <- e) がある場合、最後のジェネレータがmap、それ以外がflatMapに展開されます。


Option

Optionにもforeach / flatMap(map)がありますので 2、for式を使うことができます。

val a = Some(1)

val b = Some("abc")
val c = Some(0.5D)

for {
x <- a
y <- b
z <- c
} {
// a, b, cすべてに値が存在する場合のみ実行される
println(s"$x, $y, $z")
}

foreach / flatMap(map)に変換されるため、a, b, cのいずれかがNoneの場合、exprは実行されません。(上記の例ではexpr = println())

Noneが含まれていた場合の処理 3をfor式に対して記述するには、yieldをつけて値を返すようにすれば書けます。具体的には下記のような方法があります。

// 値を返して一旦変数に受ける

val opt = for {
x <- a
y <- b
z <- c
} yield {
s"$x, $y, $z"
}

println(opt.getOrElse("none"))

// {}式で囲む

{
for {
x <- a
y <- b
z <- c
} yield {
println(s"$x, $y, $z")
}
}.getOrElse(println("none"))

// ()でもOK
(
for {
x <- a
y <- b
z <- c
} yield s"$x, $y, $z"
) match {
case Some(s) => println(s)
case None => println("none")
}


foreach / flatMap(map)の実装について

ところで、Optionのforeach / flatMap(map)はどのようなコードになっているのでしょうか。

  @inline final def foreach[U](f: A => U) {

if (!isEmpty) f(this.get)
}

  @inline final def flatMap[B](f: A => Option[B]): Option[B] =

if (isEmpty) None else f(this.get)

  @inline final def map[B](f: A => B): Option[B] =

if (isEmpty) None else Some(f(this.get))

簡単にまとめると以下のようになっています。



  • None.foreach()の場合、引数の関数を実行せずに処理を終了


  • None.flatMap()None.map()の場合、引数の関数を実行せずにNoneを返す

つまりfor式の途中のOptionが1つでもNoneだった場合、下記のようになり、exprが実行されないわけですね。

// foreach

for {
p1 <- e1
p2 <- e2 // ここでNoneが返った場合、
: // これ以降のforeachは実行されず、exprも実行されない
:
:
pn <- en
} expr

// flatMap(map)
for {
p1 <- e1
p2 <- e2 // ここでNoneが返った場合、
: // これ以降すべて`None.flatMap()`が呼び出されてNoneが返り
: // 最後の`None.map(pn => expr)`でexprが実行されずにNoneが返る
:
pn <- en
} yield expr


util.Either

Eitherは「AまたはBのいずれか」という状態を表すのに使用する型です。

「Aか、Bか」という状態はLeftRightで表します。

(Optionは「あるか、ないか」という状態をSomeNoneで表していました)

Eitherはよく「成功か失敗のいずれか」という状態を表すのに使用します。

Rightに正常な値を格納すること前提(right-biased、右優先)で4、下記のように扱います。



  • Right
    処理が正常終了した際の値を格納する 5


  • Left
    エラーなどの処理が失敗した際の情報を格納する

case class Error(message: String)

// 何か処理(???)をしてEitherを返すメソッド
// 本来は何かしらの実装が書かれているはず
def doSomething: Either[Error, String] = ???

doSomething match {
case Right(s) => println(s"ドーモ。 $s = サン。")
case Left(Error(m)) => println(s"「エラー!?エラーナンデ!?」「$m」")
}

Right(Either.right)Left(Either.left)にもforeach / flatMap(map)が定義されているので、for式を使用することができます。

Either自体にもforeach / flatMap(map)メソッドが定義されており、前記の通りright-biasedなので、例えばEither.map()Either.right.map()と同義です。

Eitherの説明でよく使われるのは、idなどからデータを取得し、正常に取得できた場合のみ次の処理を実行して、失敗した場合はその原因を返す例です。

case class Error(message: String)

case class User(id: Int, name: String)

def getId(hash: String): Either[Error, Long] = ???
def getUser(id :Long): Either[Error, User] = ???
def getFollowers(user: User): Either[Error, Seq[User]] = ???

// Hash化されたキー(id)からいくつかの処理を経由してfollowersを取得する
val followers: Either[Error, Seq[User]] =
for {
i <- getId(hash)
u <- getUser(i)
f <- getFollowers(u)
} yield f

followers match {
case Right(f) => println(f)
case Left(Error(m)) => println(m)
}

getId()getUser()getFollowers()のいずれかでLeftが返った場合、それ以降の処理は実行されずにLeftが返ります。つまり、どの処理がどのような原因で失敗したのかがわかります。


foreach / flatMap(map)の実装について

Eitherの処理の流れはどこかで見たような感じがします。

for式で展開されるEitherのforeach / flatMap(map)のコードを見てみましょう。

  def foreach[U](f: B => U): Unit = this match {

case Right(b) => f(b)
case Left(_) =>
}

  def flatMap[AA >: A, Y](f: B => Either[AA, Y]): Either[AA, Y] = this match {

case Right(b) => f(b)
case Left(a) => this.asInstanceOf[Either[AA, Y]]
}

  def map[Y](f: B => Y): Either[A, Y] = this match {

case Right(b) => Right(f(b))
case Left(a) => this.asInstanceOf[Either[A, Y]]
}

値がLeftだった場合、下記のような動作になります。


  • foreachは、引数の関数を実行せずに処理を終了

  • flatMap(map)は、引数の関数を実行せずに自身(Left)をキャストしてそのまま返す

Optionの時と同様にfor式での動作を見てみると、下記のようになるわけですね。

// foreach

for {
p1 <- e1
p2 <- e2 // e2でLeftが返った場合、
: // これ以降のforeachは実行されず、exprも実行されない
:
:
pn <- en
} expr

// flatMap(map)
for {
p1 <- e1
p2 <- e2 // e2でLeftが返った場合、
: // これ以降すべて`Left.flatMap()`が呼び出され、このLeftがそのまま返る
: // 最後の`Left.map(pn => expr)`でもexprが実行されずにLeftがそのまま返る
:
pn <- en
} yield expr

つまりflatMap(map)の場合、Left側の型がすべて同じであれば、Eitherで処理(ジェネレータ)をどんどん繋げて書けるわけですね。

もう少し正確に言うと、Leftの型が異なるEitherのジェネレータを繋げた場合、Leftはそれらの型の共通の親である型になります。

(共通の親がいない場合、すべての型の親であるAny型になります)

Eitherで返すLeftの型を統一したい場合、Either.left.map()でLeft側の型を変換します。

case class Error(message: String)

def foo(): Either[Error, Int] = ???
def bar(): Either[String, Int] = ???

// Left を `Error` に統一する場合
(for {
i <- foo()
j <- bar().left.map(Error) // Left を `Error` に変換した Either[Error, Int] を返す
} yield i + j ) match {
case Right(sum) => println(sum)
case Left(Error(m)) => println(m)
}

// Left を `String` に統一する場合
(for {
i <- foo().left.map(_.message) // Left を `String` に変換した Either[String, Int] を返す
j <- bar()
} yield i + j ) match {
case Right(sum) => println(sum)
case Left(str) => println(str)
}


scalaz.\/

ScalazのEitherで、数学の論理和の記号(Disjunction)が由来になっています。

ScalazのEitherもright-biasedで、Either自体にforeach / flatMap(map)メソッドが定義されています。

ScalaのEitherの例をscalaz.\/で書き換えると以下のようになります。

  import scalaz.{-\/, \/, \/-}

case class Error(message: String)
case class User(id: Int, name: String)

def getId(hash: String): Error \/ Long = ???
def getUser(id :Long): Error \/ User = ???
def getFollowers(user: User): Error \/ Seq[User] = ???

// Hash化されたキー(id)からいくつかの処理を経由してfollowersを取得する
val followers: Error \/ Seq[User] =
for {
i <- getId(hash)
u <- getUser(i)
f <- getFollowers(u)
} yield f

followers match {
case \/-(f) => println(f)
case -\/(Error(m)) => println(m)
}

scalaz.\/は型引数を2つ取るclass(\/[+A, +B])なので、中置記法が使え、A \/ Bと書くことができます。

Rightは\/-で、Leftは-\/で表します。


foreach / flatMap(map)の実装について

もう大体予想がついてしまいそうですが、実際に確認してみます。

  def bimap[C, D](f: A => C, g: B => D): (C \/ D) =

this match {
case -\/(a) => -\/(f(a))
case \/-(b) => \/-(g(b))
}

// 中略

def foreach(g: B => Unit): Unit =
bimap(_ => (), g)

  def flatMap[AA >: A, D](g: B => (AA \/ D)): (AA \/ D) =

this match {
case a @ -\/(_) => a
case \/-(b) => g(b)
}

  def map[D](g: B => D): (A \/ D) =

this match {
case \/-(a) => \/-(g(a))
case b @ -\/(_) => b
}

まったく驚きのないコードかと思います。

予想通り、値がLeft(-\/)の場合の動作は下記のようになっています。


  • foreachは、bimapのLeft側に渡した何もしない関数(_ => ())が実行され、引数の関数は実行されません。

  • flatMap(map)は、引数の関数を実行せず、Left(-\/)をそのまま返します。

for式での動作もScalaのutil.Eitherと変わりありませんので省略します。


util.Try

scala.util.Tryは例外が発生するかもしれない式を引数に取り、正常に終了した場合はSuccessで値を返し、NonFatalな例外が発生した場合はFailureでその例外を返します。


Try.scala

object Try {

/** Constructs a `Try` using the by-name parameter. This
* method will ensure any non-fatal exception is caught and a
* `Failure` object is returned.
*/

def apply[T](r: => T): Try[T] =
try Success(r) catch {
case NonFatal(e) => Failure(e)
}
}

そして、util.Tryにもforeach / flatMap(map) / withFilter(filter)メソッドが定義されているため、for式が使用できます。

例えば、配列からの値取得、文字列から数値への変換、除算などは、それぞれ例外6が発生する可能性がありますが、util.Tryを使うと下記のように書くことができます。

val array = Array("1", "2")

(for {
x <- Try(array(0).toDouble)
y <- Try(array(1).toDouble)
d <- Try(x / y)
} yield d ) match {
case Success(d) => println(s"divided: $d")
case Failure(e) => println(s"Failed to calculate: $e")
}


foreach / flatMap(map)の実装について

念のため実装を確認してみましょう。

util.Tryを継承しているSuccessFailureにそれぞれ実装されているようです。


  • foreach

Success.foreach

  override def foreach[U](f: T => U): Unit = f(value)

Failure.foreach

  override def foreach[U](f: T => U): Unit = ()


  • flatMap

Success.flatMap

  override def flatMap[U](f: T => Try[U]): Try[U] =

try f(value) catch { case NonFatal(e) => Failure(e) }

Failure.flatMap

  override def flatMap[U](f: T => Try[U]): Try[U] = this.asInstanceOf[Try[U]]


  • map

Success.map

  override def map[U](f: T => U): Try[U] = Try[U](f(value))

Failure.map

  override def map[U](f: T => U): Try[U] = this.asInstanceOf[Try[U]]

動作はOptionutil.Eitherと全く同じで、Failureだった場合、下記のようになります。


  • foreachは、引数の関数を実行せずに処理を終了

  • flatMap(map)は、引数の関数を実行せずに自身(Failure)をキャストしてそのまま返す

for式での動作は省略します。


参考リンク

refactor.scala

https://gist.github.com/rirakkumya/2382341

EitherとValidation - Scalaz勉強会

http://slides.pab-tech.net/either-and-validation/

Introduction | Scalaz日本語ドキュメント

http://xuwei-k.github.io/scalaz-docs/index.html


Appendix: REPL

REPLを-Xprint:parserオプションで起動すると、for式がどのように展開されるか確認することができます。


foreach

scala> val a = Some(1)

scala> val b = Some(2)

scala> val c = Some(3)

scala> for {
| x <- a
| y <- b
| z <- c
| } {
| println(s"$x, $y, $z")
| }

[[syntax trees at end of parser]] // <console>
package $line6 {
object $read extends scala.AnyRef {
def <init>() = {
super.<init>();
()
};
object $iw extends scala.AnyRef {
def <init>() = {
super.<init>();
()
};
import $line3.$read.$iw.$iw.a;
import $line4.$read.$iw.$iw.b;
import $line5.$read.$iw.$iw.c;
object $iw extends scala.AnyRef {
def <init>() = {
super.<init>();
()
};
val res0 = a.foreach(((x) => b.foreach(((y) => c.foreach(((z) => println(StringContext("", ", ", ", ", "").s(x, y, z))))))))
}
}
}
}

// 中略

1, 2, 3


flatMap(map)

scala> val a = Some(1)

scala> val b = Some(2)

scala> val c = Some(3)

scala> for {
| x <- a
| y <- b
| z <- c
| } yield {
| println(s"$x, $y, $z")
| }

[[syntax trees at end of parser]] // <console>
package $line6 {
object $read extends scala.AnyRef {
def <init>() = {
super.<init>();
()
};
object $iw extends scala.AnyRef {
def <init>() = {
super.<init>();
()
};
import $line3.$read.$iw.$iw.a;
import $line4.$read.$iw.$iw.b;
import $line5.$read.$iw.$iw.c;
object $iw extends scala.AnyRef {
def <init>() = {
super.<init>();
()
};
val res0 = a.flatMap(((x) => b.flatMap(((y) => c.map(((z) => println(StringContext("", ", ", ", ", "").s(x, y, z))))))))
}
}
}
}

// 中略

1, 2, 3
res0: Option[Unit] = Some(())


withFilter

scala> val a = Some(1)

scala> val b = Some(2)

scala> for {
| x <- a
| y <- b
| z = x + y
| if z > 5
| } yield z

[[syntax trees at end of parser]] // <console>
package $line5 {
object $read extends scala.AnyRef {
def <init>() = {
super.<init>;
()
};
object $iw extends scala.AnyRef {
def <init>() = {
super.<init>;
()
};
import $line4.$read.$iw.$iw.a;
import $line5.$read.$iw.$iw.b;
object $iw extends scala.AnyRef {
def <init>() = {
super.<init>;
()
};
val res0 = a.flatMap(((x) => b.map(((y) => {
val z = x + y;
scala.Tuple2(y, z)
})).withFilter(((x$1) => x$1: @scala.unchecked match {
case scala.Tuple2((y @ _), (z @ _)) => z > 5
})).map(((x$2) => x$2: @scala.unchecked match {
case scala.Tuple2((y @ _), (z @ _)) => z
}))))
}
}
}
}

// 中略

res0: Option[Int] = None


Appendix: OptionとEitherの相互変換

ScalaのOptionとEitherは相互に変換可能です。


Option → Either

Option.toRight()Option.toLeft()があり、Someの時にそれぞれRightLeftを返します。

  @inline final def toRight[X](left: => X) =

if (isEmpty) Left(left) else Right(this.get)

  @inline final def toLeft[X](right: => X) =

if (isEmpty) Right(right) else Left(this.get)


Either → Option

toOptionメソッドがあり、Rightの場合にSomeが返り、Leftの場合はNoneが返ります。

  def toOption: Option[B] = this match {

case Right(b) => Some(b)
case Left(_) => None
}

Either.rightEither.leftは参照した射影(Projection)と同じEitherが入っていた場合にSomeが返ります。

(異なる場合はNoneが返ります。)

Either.toOptionEither.right.toOptionは全く同じ動作となります。

    def toOption: Option[B] = e match {

case Right(b) => Some(b)
case Left(_) => None
}

    def toOption: Option[A] = e match {

case Left(a) => Some(a)
case Right(_) => None
}

メソッドが複数あって扱いに悩むかもしれませんが、Eitherがright-biasedなので、基本的には下記のように使用するかと思います。


  • Someの時にRightを返すOption.toRight()

  • Rightの時にSomeを返すEither.toOption()


Appendix: EitherとTryの相互変換

util.Eitherutil.Tryは相互に変換可能です。


Either → Try

Leftの型がThrowableのsubtypeである場合にtoTryで変換可能です。

Either.toTry

  def toTry(implicit ev: A <:< Throwable): Try[B] = this match {

case Right(b) => Success(b)
case Left(a) => Failure(a)
}


Try → Either

Successの場合にRightが返り、Failureの場合にLeftが返ります。

Success.toEither

  override def toEither: Either[Throwable, T] = Right(value)

Failure.toEither

  override def toEither: Either[Throwable, T] = Left(exception)






  1. 雑な理解をそのまま放置してはいけない(戒め) 



  2. もちろん、withFilterもあります 



  3. 例えばログ出力処理など 



  4. 2.12.0でright-biasedに変更されました 



  5. 「正しい」という意味のRightにかかっています 



  6. それぞれjava.lang.ArrayIndexOutOfBoundsExceptionjava.lang.NumberFormatExceptionjava.lang.ArithmeticExceptionが発生する可能性があります