2012年9月3日月曜日

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

Scala 2.10で導入されたFutureはscala.concurrent.Futureです。

一方Scala 2.9段階ですでに2つのFuture、scala.actors.Futureとscala.paralell.Futureが導入されています。

scala.actors.FutureはActor用のFutureです。Scalaが元々提供していたActorは、Akkaに置き換わっていく予定なのでそれに伴いフェードアウトしていくと予想されます。

Scala 2.10のFutureはAkka(Scala Actorの後継)のFutureを包含した機能を提供し、Akkaと併用することを想定していると考えられます。

scala.paralell.FutureはScala 2.9で導入されたのですが、2.10では早くもdeprecatedになっており、scala.concurrent.Futureの利用が推奨されています。

scala.paralell.Futureは(Actorとは切り離された)一般的なfuture機能を提供することが目的だったと考えられますが、こちらの用途もscala.concurrent.Futureが一手に担うことになります。

つまり、actor用(Akka用)futureと一般用途向けfutureを一つのFutureとして提供するのがscala.concurrent.Futureの目的の一つと考えられます。

actorとfutureの関係は別途検討することとして、ここでは一般用途向けfutureという観点で調べていきます。

scala.paralell.Futureは機能的にはJavaのjava.util.concurrent.Futureと同等で、非同期実行の結果をapplyメソッドで取得(Java Futureの場合はgetメソッド)します。非同期実行をオブジェクトで隠蔽したものといえます。

それに対して2.10のscala.concurrent.Futureは何が違っているのかというと:

  • モナド化された
  • 実行コンテキストを指定できるようになった

の2点が拡張されました。いずれもScalaz Promiseの特徴的な機能で、Scalaz Promiseの機能をScala本体が取り込んだのではないかと思います。(他のフレームワークからも色々機能を取り込んでいる可能性がありますが未確認です。)

ここでは、モナド化の側面からScala Futureを見ていきましょう。

準備

準備として引数の数値×100msウェイト後、引数の数値をそのまま帰す関数gを定義します。(前回と同様)

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

性能測定用に関数goを定義します。(参考)

def go[T](a: => T): (T, Long) = {  
  val start = System.currentTimeMillis  
  val r = a  
  val end = System.currentTimeMillis  
  (r, end - start)  
}

for-comprehension

並列処理などの複雑な振舞いがモナド化されることで、いろいろな技が使えるようになると期待できますが、直接的なメリットとしてはfor式で簡単に並列処理の合成ができるようになることが挙げられます。(言うまでもありませんがScalaのfor式はモナドを簡単に操作できるようにするための文法糖衣です。)

例として、関数gを引数1, 2, 3で3回実行し、その結果の和を計算します。

まず逐次実行の場合です。この場合は計算結果が6、実行時間は約600msになりました。

scala> go { g(1) + g(2) + g(3) }
res6: (Int, Long) = (6,602)

この処理をfuture化します。

次の関数fは3つのfutureを生成し、それぞれの結果を加算したものを結果としています。前述の単純な加算に比べると複雑な構造になりますが、for式を使うことでかなり見通しよく記述できています。

def f = {
  import scala.concurrent._
  import ExecutionContext.Implicits.global
  import scala.concurrent.util.Duration

  val f1 = future { g(1) }
  val f2 = future { g(2) }
  val f3 = future { g(3) }
  go {
    val f = for {
      x <- f1
      y <- f2
      z <- f3
    } yield x + y + z
    Await.result(f, Duration.Inf)
  }
}

実行結果は以下になります。この場合は計算結果が6、実行時間は約300msになりました。関数gの最大実行時間は引数が3の場合の300msですが、これが関数fの実行時間となっています。つまり、関数gを引数1, 2, 3で3回実行していますが、この3つの実行は並列に行われたことになります。

scala> f
res4: (Int, Long) = (6,301)
もう少し複雑な例

以下のような計算を考えます。計算結果は21、実行時間は約2103msです。

scala> go { (g(1) + g(2) + g(3)) + (g(4) + g(5) + g(6)) }
res7: (Int, Long) = (21,2103)

これをFuture化すると以下になります。

def f = {
  import scala.concurrent._
  import ExecutionContext.Implicits.global
  import scala.concurrent.util.Duration

  val f1 = future { g(1) }
  val f2 = future { g(2) }
  val f3 = future { g(3) }
  val f4 = future { g(4) }
  val f5 = future { g(5) }
  val f6 = future { g(6) }
  go {
    val fx = for {
      x <- f1
      y <- f2
      z <- f3
    } yield x + y + z
    val fy = for {
      x <- f4
      y <- f5
      z <- f6
    } yield x + y + z
    val fz = for  {
      x <- fx
      y <- fy
    } yield x + y
    Await.result(fz, Duration.Inf)
  }
}

実行結果は以下になります。この場合は計算結果が21、実行時間は約600msになりました。関数gの最大実行時間は引数が6の場合の600msですが、これが関数fの実行時間となっています。つまり、関数gを引数1, 2, 3, 4, 5, 6で6回実行していますが、この6つの実行は並列に行われたことになります。

for式で得られたFutureをさらにfor式で合成しても、並列処理の性質は損なわれることなく無事並列動作することが確認できました。

scala> f
res5: (Int, Long) = (21,601)

ノート

Scala 2.9のscala.paralell.Futureはオブジェクト指向的なfutureですが、モナドでないため関数型プログラミングとの相性が今ひとつでした。その問題を解決したのがScalaz 6のPromiseです。

Scala 2.10のscala.concurrent.Futureは、モナドとして実装されたfutureで関数型プログラミングで効果的に使えるようになっています。Scalaz 6 PromiseをScala本体が取り込んだという見方もできるかと思います。

逆にScala 2.10上でScalaz Promiseをどう扱っていくのかというのが新たな論点となってきます。

そういう目でScalaz 7を眺めてみると、Promiseは残っているもののcoreからは分離されconcurrentパッケージとして提供されておりオプション的な扱いになっています。特に象徴的なのは、coreのscalaz.Function1Wにあったpromiseメソッドがscalaz.syntax.std.Function1Opsには残っていない点です。(kleisliメソッドはそのまま残っています。)Scalaz Promiseの入っているconcurrentパッケージはJARも別になるので、意識して設定しないと使えない機能になっています。

このような状況なので、Scalaz 7はScala 2.10 Futureを並列プログラミングの基本機能として併用することを想定しているのではないかと推測できます。ただ、前述した通りscalaz.Function1Wにあったpromiseメソッドがなくなっており、またscalaz.MAで定義されていたparMap, parBind, parZiWithメソッドもありません。そういう意味では、Scalaz 7とScala 2.10 Futureとのシームレスな連携は今後の課題ということになります。

Scala 2.10 Futureはモナドなので、つなぎコードを整備すればScalazのkleisli, applicative functor, traverse, monoidといった機能を連携させることは容易と考えられます。Scala 2.10が落ち着いて、Scalaz 7がこういった部分の整備が一段落したところが、Scala 2.10+Scalaz 7への移行のタイミングの目安と考えたいと思います。

諸元

  • Scala 2.10.0-M7
  • Scalaz 7 github 2012-09-03版

4 件のコメント:

  1. > いずれもScalaz Promiseの特徴的な機能で、Scalaz Promiseの機能をScala本体が取り込んだのではないかと思います。(他のフレームワークからも色々機能を取り込んでいる可能性がありますが未確認です。)


    SIP14のdocument
    http://docs.scala-lang.org/sips/pending/futures-promises.html
    を見ると、Scalazへの言及があるので、もちろん影響は受けているとは思いますが、「Scalazだけ」ではないというか、他のライブラリの影響もある程度存在するかと思います。そもそもSIP14のFitureの統一の目的が「様々なライブラリにfitureがあるが、かなり似ているし、統一したほうがメリットがある」
    という感じでしょうから。SIP14のdocumentには浅海が挙げていない、twitter社のFinagleのFutrueについても触れらています。

    返信削除
  2. はい。その点は認識していますよ。
    前回の記事でもその点には言及しています。
    今回の記事の該当部分は、少なくてもScalazのPromiseは取り込んでいるね、という意味で、他のフレームワークも取り込んでいる可能性はあるけど未確認です、ということです。

    返信削除
  3. すいません、慌てて書いたら、なぜか呼び捨てになってしまいました

    s/浅海/浅海さん

    返信削除
  4. あ、Finagleについては前回の記事で触れられていたのですね、失礼しました

    返信削除