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が発生する可能性があります 

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.