BETA

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

投稿日:2020-04-17
最終更新:2020-04-17

概要

  • この記事は 2016年04月14日 にQiitaへ投稿した内容を転記したものです。
  • 本記事は執筆から1年以上が経過しています。

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でその例外を返します。

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)  

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

@QeSaxrznJ6PLUYgjの技術ブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう