状態遷移とコンピュテーション式


This is the sixth post of the F# Advent Calendar 2014 (in Japanese). English translation of the post will be available here.

この記事は「F# Advent Calendar 2014」の 6 日目の記事です。前日は bleis-tift さんの『実例に見るSource変換活用術』でした。

今年はコンピュテーション式の話が多いような気もしますが,今日もコンピュテーション式の話です。

コンピュテーション式といえばモナドを記述するための構文という見方が強いですが,一方でドメイン特化言語 (DSL) を記述するための構文として使えるということも忘れてはならないと思うのです。 F# 3.0 より導入された CustomOperationAttribute を用いることで,キーワードを増やすことができるため,コンピュテーション式の枠組みの中において,計算の組み立てが容易にかつ安全に行えるわけです。

カスタムオペレーションを使ったビルダーの単純な例を以下に示します。

type CustomBuilder () =
   member __.Yield (x) = x
   [<CustomOperation("Op")>]
   member __.CustomOperation (x, arg) = arg
let custom = CustomBuilder ()

custom {
   Op "foo"
   Op "bar"
}

CustomOperation の変換規則は Language Spec を見てもらえばよいのですが,以下のように変換されます。

custom.CustomOperation (
   custom.CustomOperation (
      custom.Yield ( () ),
      "foo"
   ),
   "bar"
)

最初にユニットが Yield されていることはさておき,前の計算の結果を CustomOperation を指定したカスタム操作メソッドの第 1 引数に,そしてコンピュテーション式でキーワードの後ろに与えた値がメソッドの第 2 引数に渡されていることがわかります。つまりざっくり言うと,前の状態を受け取って,何らかのパラメーターを与えることで次の状態を返すという関数を作成しているのに相当します。ビルダークラスには同様にしてたくさんのカスタム操作メソッドを追加することができます。つまり,前の状態を受け取って次の状態を返す複数の関数をビルダークラスは持つことができます。言い換えると,ビルダークラスはさまざまな状態遷移を持つことができます。

ところで Yield は何でしょうか。最初に呼ばれるものですから,これは初期状態を表すものと考えることができます。つまり,コンピュテーション式でカスタム操作により計算を組み立てるときは,初期状態 (Yield) と状態遷移 (カスタム操作) を記述するということになります。

もう少し具体的なシチュエーションで考えてみます。今東西南北の 4 方向のいずれかを向いているという状態を考えます。

type Direction = North | East | South | West

ある方角に向かう,右を向く,左を向くという動作を考えると,これは状態変化を表すので,前述のコンピュテーション式の方法で記述することができます。

type DirectionBuilder () =
   // 最初は北を向く
   member __.Yield (_) = North
   [<CustomOperation("Turn")>]
   member __.Turn (_, direction) = direction
   [<CustomOperation("TurnLeft")>]
   member __.RotateLeft (direction) =
      match direction with
         | North -> West
         | East -> North
         | South -> East
         | West -> South
   [<CustomOperation("TurnRight")>]
   member __.RotateRight (direction) =
      match direction with
         | North -> East
         | East -> South
         | South -> West
         | West -> North
let direction = DirectionBuilder ()

初期状態は北を向いているものとします。右を向いて南を向いて左を向いて左を向いて右を向くというような状態遷移は以下のように書けます。

direction {
   TurnRight
   Turn South
   TurnLeft
   TurnLeft
   TurnRight
}
|> printfn "%A"  // East

こんな感じでコンピュテーション式を状態遷移ととらえることで,人によっては理解しやすくなるのではないかなと思った次第です。