twitter-util Spool の紹介

この記事は Play or Scala Advent Calendar 2012 の9日目です。

はじめに

twitter-util には cuncurrent に関する基本的なクラスが提供されています。Scala本体に取り込まれる予定の `Future` や、非同期にオブジェクトを仲介する `Offer` や `Broker` などはよく知られているかと思います。

今日はそんな中であまり話題に上らない `Spool` を紹介したいと思います。

Spool とは

`Spool` は `Stream` とそっくりな lazy な評価戦略を持つ非正格なリストデータ構造です。`Stream` との決定的な違いは、要素の評価計算が非同期に行われるという所です。

つまり、 `head` の呼び出しで先頭の1要素を取得しようとした場合には、同期呼び出しで即座に値が返されますが、`tail` で2件目以降の要素を取得しようとすると、`Future[Spool[A] ]` 型の `Future` が即座に返却されます。実際の `Spool[A]` を取得するにはこの `Future` の完了を待つ必要があります。

それ以外は `Stream` とあまり変わりません。内部的には非同期に解決されますが、`foreach` や `map` といったメソッドも持っています。また、`Stream` が `#::` といったメソッドと Extractor を持っているように、`Spool` にも `*::` と `**::`というメソッド及び Extractor があります。

サンプル

試しに REPL で遊んでみましょう。前準備として、呼び出したら非同期に3秒後の `Date` を返してくれる `threeSecondsAfter` メソッドを定義してみます。

scala> :paste
// Entering paste mode (ctrl-D to finish)

import com.twitter.util._
import java.util.Date
import com.twitter.conversions.time._
import com.twitter.concurrent._
import com.twitter.concurrent.Spool._

implicit val timer = new JavaTimer
def threeSecondsAfter: Future[Date] = timer.doLater(3.seconds)(new Date)

// Exiting paste mode, now interpreting.

import com.twitter.util._
import java.util.Date
import com.twitter.conversions.time._
import com.twitter.concurrent._
import com.twitter.concurrent.Spool._

timer: com.twitter.util.JavaTimer = com.twitter.util.JavaTimer@ecfd5a
threeSecondsAfter: com.twitter.util.Future[java.util.Date]

scala>

`threeSecondsAfter` の 戻り値型は `Future[Date]` なので `onSuccess` で実際の値を確認します。

scala> threeSecondsAfter.onSuccess(println)
res0: com.twitter.util.Future[java.util.Date] = Promise@24539713(ivar=Ivar@21448365(state=Waiting(List(),List())), cancelled=Ivar@3420708(state=Waiting(List(<function1>),List())))

scala> Sun Dec 09 22:27:35 JST 2012

`onSuccess` が即座に評価されて res0 として表示された後、3秒後に日時が表示されるのが確認できるかと思います。

っていうかもう22時過ぎてる!やばい!Advent Calendarの担当日過ぎてしまう!

気を取り直して、このメソッドを使って、3秒間隔の `Date` の無限列 つまり `Spool[Date]` を作ってみましょう。

scala> def threeSeconds: Future[Spool[Date]] = threeSecondsAfter.map(_ *:: threeSeconds)
threeSeconds: com.twitter.util.Future[com.twitter.concurrent.Spool[java.util.Date]]

`Stream` の `#::` と同じように `*::` メソッドで先頭に要素を追加することができます。これで `Spool[Date]` が手に入りました。これの要素を全て表示してみましょう。

scala> threeSeconds.onSuccess(_.foreach(println))
res2: com.twitter.util.Future[com.twitter.concurrent.Spool[java.util.Date]] = Promise@25639455(ivar=Ivar@16315671(state=Waiting(List(),List())), cancelled=Ivar@10668522(state=Waiting(List(<function1>),List())))

scala> Sun Dec 09 22:37:31 JST 2012
Sun Dec 09 22:37:34 JST 2012
Sun Dec 09 22:37:37 JST 2012
Sun Dec 09 22:37:40 JST 2012
Sun Dec 09 22:37:43 JST 2012
Sun Dec 09 22:37:46 JST 2012
Sun Dec 09 22:37:49 JST 2012
Sun Dec 09 22:37:52 JST 2012
Sun Dec 09 22:37:55 JST 2012

REPL上で `foreach` が解決された後に延々に3秒間隔で表示されるのがわかるかと思います。こんな感じで `Spool` を使う事で、非同期な計算を必要とする処理のシーケンスを得る事ができます。

実用例

さてさて、この `Spool` を使用しようした具体例として、これから発生するであろうイベントの連続を `Spool` を使って表すというものがあります。

Finagle には、Zookeeper を利用してサーバクラスタを構成する機能があります。このクラスタを表現するためのクラスが、 `Cluster` なのですが、この `Cluster` は `snap` というメソッドを持っています。

この `snap` メソッドの戻り値型は `(Seq[T], Future[Spool[Cluster.Change[T] ] ])` となっています。タプルの第一要素は現在のクラスタを構成しているオブジェクトの集合を表しています。そして第二要素が、これからこのクラスタに訪れるであろう変更イベント`Cluster.Change[T]`つまり、クラスタへの参加や離脱といったイベントですね、これらの `Spool` となっています。

この `Spool` を使う事で、`snap` を呼び出した側は現在のクラスタ構成だけでなく、将来に渡るクラスタの変更にも対応できるようになっているのです。

まとめ

という訳で、lazy かつ非同期な評価戦略を持つシーケンスである `Spool` の紹介でした。個人的には中々面白いデータ構造だと思っています。使う場所によっては `Broker` 等よりも使う側が楽なインターフェイスを提供できるかと思います。

みなさんも是非 `Spool` で遊んでみてください。

2013-01-04 18:00 修正

一部用語の使い方が正しくなかったので修正。参考: 遅延評価いうなキャンペーンとかどうか