a wandering wolf

Does a wandering wolf dreams of a wondering, sometimes programming sheep?

このエントリーをはてなブックマークに追加

Actor ライブラリ Cricket のご紹介 #FsAdvent

この記事はF# Advent Calendar 2015の29日目の記事です。28日目は@pocketbersekerさんの「fsugjp/publicというGitter roomについて - pocketberserkerの爆走」でした。

この記事の目的

Actorライブラリ注目マンであるところの私が、F# のActorライブラリであるCricketについてご紹介します。

Cricketとは

CricketはF#製のActorライブラリです。

Cricket

F#で使えるActorライブラリはいくつかあり、標準ライブラリであるMailboxProcessor<’Msg>の他に、JVMの世界からやってきたAkka.NET、Microsoft Researchが新たに提供するOrleans などがあります。ここではそれらの比較はしませんが、それぞれに特徴のあるライブラリたちだと思います。

Cricketの特徴は、F#によるF#のためのライブラリなので、アクターをコンピュテーション式で書けるということと、あとはアクターの雰囲気がErlangの「プロセス」にように思われること、です。なお、現時点ではF# 3.0でのみ動作しているようです。

サンプルコード

それでは、公式ドキュメントにあるサンプルコードを見てみましょう。

open Cricket

ActorHost.Start()

type Say =
    | Hello
    | HelloWorld
    | Name of string

let greeter =
    actor {
        name "greeter"
        body (
            let rec loop() = messageHandler {
                let! msg = Message.receive()  // メッセージを待ちます

                match msg with
                | Hello ->  printfn "Hello"             // Helloの場合
                | HelloWorld -> printfn "Hello World"   // HelloWorldの場合
                | Name name -> printfn "Hello, %s" name // Nameの場合

                return! loop() // 再帰的にループします

            }
            loop())
    } |> Actor.spawn

greeter <-- Name("from F# Actor")

これを実行すると、次のように表示されるかもしれません:

[D] 2015-12-29T05:14:23.9702664Z: ActorHost started YOURMACHINE_YourProject_7368 [YOURMACHINE_YourProject_7368]
Hello, from F# Actor

最初のActorHost.Start()で行う初期化が重いようで、ここで少し時間がかかりますが、それが終わるとささっと流れます。(コンソールに何も表示されないようでしたら、最後の行でスリープを噛ませるといいでしょう)

アクターはactor { .. }というコンピュテーション式で作ります。カスタムオペレーションのnamebodyは、それぞれアクターの名前と処理本体を設定するものです。bodyの中では、MailboxProcessor<'Msg>などでもやるような、ループ関数を作っています。Cricketの場合、ループ関数を定義する際に使うコンピュテーション式が、async { .. }ではなくmessageHandler { .. }である、という違いがあります。

アクターの動的ディスパッチ

さて、アクターを定義する際に名前を付けたのですが、これは一体何に使うのでしょうか。それは、「その名前で呼べるようにするため」です。意味不明だと思うので、次のコードをご覧ください:

let resolvedGreeter = !!"greeter"
resolvedGreeter <-- Hello

先のサンプルコードで、nameカスタムオペレータに”greeter”という文字列を渡していました。この名前の文字列で、対応するアクターの選択を動的に解決することができます。

なお、変数に束縛する必要がなければ、次のように書くこともできます:

"greeter" <-- Hello

アクターを動的に呼び出せるようになって一番嬉しいこと、それは「アクター間でメッセージのやりとりをしやすくなる」ことだと思います。次のコードを見てみましょう:

type Message =
    | Send of string
    | Stop

let white = actor {
    name "white"
    body (
        let black = !~"black"

        let rec loop count =
            messageHandler {
                let! msg = Message.receive()
                match msg with
                | Send _ when count < 5 ->
                    printfn "白ヤギさんは手紙を食べました。(通算%d回目)" count
                    do! Message.post black.Value <| Send("さっきのてがみのごようじなあに")
                    return! loop (count + 1)
                | _ -> do! Message.post black.Value Stop
            }
        loop 1
    )
}

let black = actor {
    name "black"
    body (
        let white = !~"white"

        let rec loop count =
            messageHandler {
                let! msg = Message.receive()
                match msg with
                | Send _ ->
                    printfn "黒ヤギさんは手紙を食べました。(通算%d回目)" count
                    do! Message.post white.Value <| Send("さっきのてがみのごようじなあに")
                    return! loop (count + 1)
                | _    -> ()
            }
        loop 1
    )
}

ActorHost.Start().SubscribeEvents(fun (evnt: ActorEvent) -> printfn "%A" evnt) |> ignore
let whiteSheep = Actor.spawn white
let blackSheep = Actor.spawn black

whiteSheep <-- Send "やぎさんゆうびん"
Async.Sleep 1000 |> Async.RunSynchronously

このコードを実行すると、次のように表示されるかもしれません:

[D] 2015-12-29T05:58:05.8222278Z: ActorHost started MYMACHINE_MyProject_9360[MYMACHINE_MyProject_9360]
白ヤギさんは手紙を食べました。(通算1回目)
黒ヤギさんは手紙を食べました。(通算1回目)
白ヤギさんは手紙を食べました。(通算2回目)
黒ヤギさんは手紙を食べました。(通算2回目)
白ヤギさんは手紙を食べました。(通算3回目)
黒ヤギさんは手紙を食べました。(通算3回目)
白ヤギさんは手紙を食べました。(通算4回目)
黒ヤギさんは手紙を食べました。(通算4回目)
ActorStarted (ActorRef actor://MYMACHINE_MyProject_9360@*/white)
ActorStarted (ActorRef actor://MYMACHINE_MyProject_9360@*/black)
ActorShutdown (ActorRef actor://MYMACHINE_MyProject_9360@*/white)
ActorShutdown (ActorRef actor://MYMACHINE_MyProject_9360@*/black)

ある程度F#のアクターに親しんでいる方にはそれほど難しくないコードだと思います。特徴的なところは、それぞれのアクターで (!~) という演算子を使っている箇所だと思います。これは先程見た (!!) 演算子のLazy版です。遅延評価するので、1つ目のアクターを定義する際に、2つ目のアクターがまだ読み込まれていなくても問題ありません(正格評価版を実行すると異常終了します)。これにより、2つのアクター同士でメッセージを送り合うことができるのです。

MailboxProcessor<'Msg>にはこの機能がないため、同じことをやろうとすると不必要な手間がかかってしまうのですが、Cricketではそれをほとんど感じさせません。

まとめ

F#製ActorライブラリであるCricketを手短にご紹介しました。

まだアルファ版ということで改善の余地があるプロダクトだとは思いますが、機能的に非常に面白みのあるライブラリで、今後も注目していきたいと思います。