Scalaのアクターモデル

アクターモデルは最後らへんかなー…と思ってたら、ScalaByExampleのChapter3はいきなりアクターモデルの話(Programming with Actors and Messages)だった。ちなみにChapter2は言語仕様の特徴的な部分のOverviewのような感じ。

Chapter3ではまず、丁寧に(Erlangスタイルの)アクターについて説明してくれている。

Actors are objects to which messages are sent. Every actor has a “mailbox” of its incoming
messages which is represented as a queue. It can work sequentially through
the messages in itsmailbox, or search for messages matching some pattern.

ということで、アクターというのは…
  • メッセージを受信するオブジェクト
  • キューで表現されるメールボックスを持っている
  • キュー内のメッセージを通じて順次動作する、またはパターンマッチでメッセージを選択して動作する
というものらしい。勉強になります。このドキュメントはScalaByExampleなので、以降はネットオークションのシステムを例として説明が進んでいく模様。

アクター
まずはネットオークションのシステムに存在するアクターとして、
  • Auctioneer(オークションを仕切る人)
  • Client(落札しようとする人)
というのが想定されている。Auctioneerは、オークションアイテムに関する情報を提供したり、オファーを受けたり、出品者と落札者の間を取り持ったりする。

メッセージ
で、まずはメッセージを決めるらしい。素人はおおせのままにするしかない。ぶっちゃけていうと、以下の二種類のメッセージが必要になる。
  • AuctionMessage
  • AuctionReply
へぇ。で、この二種類のメッセージが持つバリエーションが以下のようになる。

import scala.actors.Actor
abstract class AuctionMessage
case class Offer(bid: Int, client: Actor) extends AuctionMessage
case class Inquire(client: Actor) extends AuctionMessage

abstract class AuctionReply
case class Status(asked: Int, expire: Date) extends AuctionReply
case object BestOffer extends AuctionReply
case class BeatenOffer(maxBid: Int) extends AuctionReply
case class AuctionConcluded(seller: Actor, client: Actor) extends AuctionReply

case object AuctionFailed extends AuctionReply
case object AuctionOver extends AuctionReply
なんということだ。case classとか良くわかってないんだが…。しょうがないからTutorialに戻ろう…。
case calssesの特徴は、
  • インスタンス化にnewキーワードが不要
  • コンストラクタ仮引数に対するgetterメソッドは自動的に定義される
  • (IDベースでなく)値ベースのequals/hashCodeが自動的に定義される
  • いけてるtoStringが自動的に定義される
  • これらのクラスのインスタンスは、パターンマッチングを通じて分解できる
最後のやつ以外はどうでもいいっぽい。で、

abstract class Tree
case class Sum(l: Tree, r: Tree) extends Tree
case class Var(n: String) extends Tree
case class Const(v: int) extends Tree

代数演算を行うための構造をこんな感じで定義したとする。Javaだと型階層で表現するところ。これがパターンマッチングを通じて分解できるとは、

def eval(t: Tree, env: Environment): int = t match {
case Sum(l, r) => eval(l, env) + eval(r, env)
case Var(n) => env(n)
case Const(v) => v
}

こういうことらしい。これって嬉しいのか…?バリエーションの制御を型にカプセル化するんじゃなく、操作の方に書いちゃってるけど。と思ったら、使い分けの方法も書いてある。

Deciding whether to use pattern matching or methods is therefore a matter of taste, but it also has important implications on extensibility:
  • when using methods, it is easy to add a new kind of node as this can be done just by defining the sub-class of Tree for it; on the other hand, adding a new operation to manipulate the tree is tedious, as it requires modifications to all sub-classes of Tree,
  • when using pattern matching, the situation is reversed: adding a new kind of node requires the modification of all functions which do patternmatching on the tree, to take the new node into account; on the other hand, adding a new operation is easy, by just defining it as an independent function.
端的に言うと…
  • 継承を使った場合、新しいノード(型)を追加するのは簡単な一方で、新しい操作を追加するのは困難になる。型階層の上位に操作が新規に定義された場合には影響が広範囲に及ぶ。多態性をうまく使っていればなおさら。
  • case classes+パターンマッチングは継承とは逆で、新たなノード(型)を追加するのは難しい。いろいろな場所の操作でcaseを追加する必要があるから。ただし、操作を追加するのは簡単。単に新たな関数を追加すれば良い。
というわけで、何が可変性なのかを意識し、その可変性が「概念そのもの」ではなく「操作」である場合には、型階層よりもうまく扱うことができるメカニズムだというわけですね。やー、さすが後発言語。
というわけで、Messageに戻ろう。と思ったらMessageはこれで終わりかよ!

アクターの実装
てなわけで、もうアクターの実装がでちゃいました。

class Auction(seller: Actor, minBid: Int, closing: Date) extends Actor {
val timeToShutdown = 36000000 //msec
val bidIncrement = 10

def act() {
var maxBid = minBid bidIncrement
var maxBidder: Actor = null
var running = true
while (running) {
receiveWithin ((closing.getTime() - new Date().getTime())) {
case Offer(bid, client) =>
if (bid >= maxBid + bidIncrement) {
if (maxBid >= minBid) maxBidder ! BeatenOffer(bid)
maxBid = bid; maxBidder = client; client ! BestOffer
} else {
client ! BeatenOffer(maxBid)
}
case Inquire(client) =>
client ! Status(maxBid, closing)
case TIMEOUT =>
if (maxBid >= minBid) {
val reply = AuctionConcluded(seller, maxBidder)
maxBidder ! reply; seller ! reply
} else {
seller ! AuctionFailed
}
receiveWithin(timeToShutdown) {
case Offer(_, client) => client ! AuctionOver
case TIMEOUT => running = false
}
}
}
}
}
アクターはどうやら、Actorというクラスを継承するようだ。アクターの動作はact()で定義されているが、これはActorクラスで定義されたメソッドだろうか。scala.actors.Actorクラスのソースを見てみると、ビンゴのようだ。Actorsフレームワークみたいな感じなのね。

receiveWithinメソッドもActorで定義されているメソッドで、引数で渡された時間内に、ブロックを処理するようにスケジュールするらしい。
メインの処理は、1つめのreceiveWithinのcase Offer ~ case Inquireが相当し、オークション開催時間内に実行される処理が定義されている。

引き渡された時間内にメッセージが到着しなかった場合、自身に向けてTIMEOUTメッセージを送信する。これに対応した処理がcase TIMEOUT~の部分で、再びreceiveWithinを使ってオークションの終了処理を定義している。

基本的にはわかりやすいんだけど、receiveWithinについていくつかわからないことが。このメソッドの宣言は
  def receiveWithin[R](msec: Long)(f: PartialFunction[Any, R]): R =
self.receiveWithin(msec)(f)

こんな感じになっているんだけど、このPartialFunctionがどういう仕様になっているのかがよくわからない。Rubyのブロックみたいなもんだと思うんだけど、ブロック内のcaseからなぜメッセージにアクセスできるのかがよくわからないのだ。このあたりはこのドキュメントのこの段階ではあまり触れられていない。

scala.actors.Reactionの f(msg) ってあたりを見ると、引数で渡してるみたいなんだけど…ブロック引数いまいちわからず。このあたりはまた先でわかるといいな。

コメント