2012年9月5日水曜日

Scala Tips / Scala 2.10味見(6) - Future(4)

Scala 2.10のFutureはモナドとして定義されていて、モナド的ないろいろな技が使えるようになっています。(参考:「Scala Tips / Validation (11) - モナド」)

また、Futureのコンビネータ、ユーティリティメソッドまわりは有用そうな機能が満載で、本来はこのあたりの基本機能から入るべきですが、個人的な趣味で今回はScalaz的なMonadicプログラミング向けの機能を取り上げます。

準備

例によって以下の関数を使います。

val g = (x: Int) => {  
  Thread.sleep(x * 100)
  x  
}

REPL上で以下の設定が行われているものとします。

import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.util.{Success, Failure}
import scala.concurrent.util.Duration

sequence

モナドを使い出すと、モナドの入れ子をどうさばくのかというのがプログラミング上のテクニックとなってきます。

そこで登場するのがScalazのsequenceメソッド。「Scala Tips / Validation (22) - sequence」など、このブログでも再三取り上げました。sequenceメソッドはモナドの入れ子(正確にはTraverseとApplicative)を扱う場合の定番イディオムとなっています。

たとえば、ListとSomeの入れ子は以下のように操作することができます。

scala> List(1.some, 2.some, 3.some).sequence
res309: Option[List[Int]] = Some(List(1, 2, 3))
Scala 2.10 Future

seqeunceのFuture版です。FutureのListから関数のList全体をFuture化しています。

scala> val a = List(future(g(1)), future(g(2)), future(g(3)))
a: List[scala.concurrent.Future[Int]] = List(scala.concurrent.impl.Promise$DefaultPromise@58276cfe, scala.concurrent.impl.Promise$DefaultPromise@464c4e9, scala.concurrent.impl.Promise$DefaultPromise@73bb9f3f)

scala> val b = Future.sequence(a)
b: scala.concurrent.Future[List[Int]] = scala.concurrent.impl.Promise$DefaultPromise@1b45fed7

scala> Await.result(b, Duration.Inf)
res26: List[Int] = List(1, 2, 3)
Scalaz 6 Promise

参考にScalaz 6のPromiseです。Scala 2.10 Futureとほとんど同じになります。

scala> val a = List(promise(g(1)), promise(g(2)), promise(g(3)))
a: List[scalaz.concurrent.Promise[Int]] = List(<promise>, <promise>, <promise>)

scala> val b = a.sequence
b: scalaz.concurrent.Promise[List[Int]] = <promise>

scala> b.get
res306: List[Int] = List(1, 2, 3)

traverse

traverseメソッドsequenceメソッドの親玉になるメソッドで、モナドの入れ子に対してより汎用的な処理を行うことができます。

scala> List(1, 2, 3).traverse(x => (x + 1).some)
res315: Option[List[Int]] = Some(List(2, 3, 4))
Scala 2.10 Future

traverseのFuture版です。Listに対してfuture化した処理を適用して、関数のList全体をFuture化しています。

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

scala> val b = Future.traverse(a)(x => future(g(x)))
b: scala.concurrent.Future[List[Int]] = scala.concurrent.impl.Promise$DefaultPromise@1d9b0076

scala> Await.result(b, Duration.Inf)
res27: List[Int] = List(1, 2, 3)
Scalaz 6 Promise

参考にScalaz 6のPromiseです。Scala 2.10 Futureとほとんど同じになります。

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

scala> val b = a.traverse(g.promise)
b: scalaz.concurrent.Promise[List[Int]] = <promise>

scala> b.get
res307: List[Int] = List(1, 2, 3)

ノート

まず、Futureのコンビネータ、続けてユーティリティメソッドを取り上げるつもりだったのですが、ユーティリティメソッドにsequenceとtraverseを見つけてしまい、先にこれを取り上げることにしました。

Scalazではsequenceやtraverseは、型クラスTraverseとApplicativeに対する処理でざっくりいうとTraverse[Applicative[A]]をApplicative[Traverse[A]]に変換する処理を行います。この処理が、モナドの入れ子操作が頻出するMonadicプログラミングでは非常に便利なわけです。

Scala 2.10 Futureでは、このsequenceとtraverseをTraversableOnceとFutureへのハードコーディングで実現しています。TraversableOnceはすべてのコレクションクラスの総親分なので、コレクション全体を対象にしているので実用的な意味での適用範囲は広いですが、Scalazのような型クラスを用いたアプローチより汎用性が制限されることになります。

Futureのsequenceやtraverseを見て感じたのは、モナドを効率よく操作するためにはこういったScalaz的(Haskell的)な機能は必要ということです。Futureだけのことを考えると、今回の実装のようなハードコーディングで凌ぐことができますが、これから陸続と新しいモナドがクラスライブラリに追加されてくることを考えると、いずれ破綻してしまうのは明らかです。

こうなった場合の対応策は、Scalazが採っているHaskell流の型クラス方式ということになるでしょう。Scalaの基本ライブラリがScalaz的なアプローチを取り入れるのか、Scalazが事実上の基本ライブラリとして併存するようになるのか、分かりませんが、モナドの利用が進むに従って型クラス方式の傾倒が進むのではないか。というのが、sequenceメソッド、traverseメソッドを見ての感想です。

諸元

  • Scala 2.10.0-M7
  • Scalaz 6.0.4

0 件のコメント:

コメントを投稿