2012年11月1日木曜日

Scala Tips / flatMapとbind(>>=)の違い

ScalaのflatMapメソッドは、モナドのbindに対応するメソッドとして認識されています。つまり、基本的にはScalazの>>=メソッドと同じ動作をするわけですが、微妙な機能差があります。

以下はListに対してflatMapメソッドを用いてモナドのbind処理を行ったものです。

scala> List(1, 2, 3).flatMap(x => if (x % 2 == 0) List(x, x) else Nil)
res16: List[Int] = List(2, 2)

これはScalazの>>=メソッドも全く同じ動作をします。

scala> List(1, 2, 3) >>= (x => if (x % 2 == 0) List(x, x) else Nil)
res17: List[Int] = List(2, 2)

次の例

さて、今度の例はListのコンテナに対してOptionをflatMap関数で適用しています。これも想定通りの動作をしました。

scala> List(1, 2, 3).flatMap(x => if (x % 2 == 0) Option(x) else None)
res18: List[Int] = List(2)

しかし、Scalazの>>=メソッドでは文法エラーになってしまいました。

scala> List(1, 2, 3) >>= (x => if (x % 2 == 0) Option(x) else None)
<console>:14: error: type mismatch;
 found   : Option[Int]
 required: List[?]
              List(1, 2, 3) >>= (x => if (x % 2 == 0) Option(x) else None)
                                                            ^
<console>:14: error: type mismatch;
 found   : None.type
 required: List[?]
              List(1, 2, 3) >>= (x => if (x % 2 == 0) Option(x) else None)

なぜ、このような結果になってしまうのでしょうか。

モナドは単なる自己関手の圏におけるモノイド対象だよ。」という有名な?説明がありますが、モナドは俗っぽい言い方をするとコンテナ(モナド)の中にコンテナが入っている構造で、外側のコンテナと内側のコンテナが同じ型の時に一つにまとめる事ができるものです。

ここで重要なのは、外側のコンテナと内側のコンテナが同じ型でなければならないという前提条件です。「List(1, 2, 3) >>= (x => if (x % 2 0) Option(x) else None)」が文法エラーになってしまうのは、外側のコンテナがList、内側のコンテナがOptionで、(各々はモナドではあるものの)型が違うためですね。

Scalazの>>=メソッドはモナドのbindとしては正しい動作になっているわけです。

flatten

ScalaではListなどのコレクションはflattenメソッドを提供しています。flattenメソッドは、外側のコンテナと内側のコンテナの型の相違は気にせず、以下のように内側のコンテナを平坦化する処理を行います。

scala> List(None, Some(2), None).flatten
res20: List[Int] = List(2)

これをflatMapメソッドの動作と同じになるようにしてみると以下になります。flatMapメソッドは文字通りmapした後にflattenする処理を行うわけですね。

scala> List(1, 2, 3).map(x => if (x % 2 == 0) Option(x) else None).flatten
res23: List[Int] = List(2)

flatMapメソッドは基本的にはモナドのbindと考えておいてよいですが、flatMapメソッドの適用範囲がモナドより少し広いことを知っておくとプログラミングの幅が広がります。

特に、外側のコンテナがList(といったSeq)、内側のコンテナがOptionの組合せはScalaプログラミングでは頻出なので、flatMapメソッド(あるいはflattenメソッド)でこの組合せを捌く方法はScalaプログラマの必須イディオムといえます。

諸元

  • Scala 2.10.0-RC1
  • Scalaz 2.10.0-M6

2 件のコメント:

  1. ScalaのflatMapも型が合わないとコンパイルエラーです。
    flatMapの定義は以下です。

    def flatMap[B, That](f: A => scala.collection.GenTraversableOnce[B])(implicit bf: generic.CanBuildFrom[Repr,B,That]): That

    冒頭の例ではOptionがGenTraversableOnceにimplicit conversionされるので、たまたま型があってなくても動いているように見えるだけです。
    例えば、以下の例ではエラーです。

    scala> Some(1).flatMap{ _ => List(1) }
    :8: error: type mismatch;
    found : List[Int]
    required: Option[?]
    Some(1).flatMap{ _ => List(1) }
    ^
    List[Int] <: Option[?]?
    false

    scala> Some(1).toList()
    Unit <: Int?
    false
    :8: error: not enough arguments for method apply: (n: Int)Int in trait LinearSeqOptimized.
    Unspecified value parameter n.
    Some(1).toList()
    ^

    返信削除
  2. モナドの定義として、一時的にではあれflatMap内部で、型がGenTraversableOnceを受け入れるようになっているのは、確かにモナドのbindとして正しくはないです。
    ただ、flatMapはGenTraversableOnceのCanBuildFrom型クラスのインスタンスを要求するようになっているので、最終的な結果型は保存されます

    返信削除