もう一山登るぞcalculatorActions展開編


 Add関数の正体が分かったところで、残ったcalculatorActionsの征服にかかりましょう。「Programming F#」ではもう少し式が加えられていますが、何行並んでいても本質的には同じなので、returnを入れて3行の式とします。これをVisualStudioに載せてコンパイルが通り、
let res, stat = Run calculatorActions (0, [])
の実行結果を確認するところから始めましょう。

let calculatorActions =
  state {
    do! Add 2
    do! Multiply 3
    return "完了!"
  }

1.Add、Multiplyの展開

 まず小手調べに、AddとMultiplyを「Add展開編」で出来上がった最後の式を使い展開します。

let calculatorActions =
  state {
    do! StatefulFunc(fun (currentTotal, history) -> (), (currentTotal + 2, (sprintf "%dを加算" 2) :: history))
    do! StatefulFunc(fun (currentTotal, history) -> (), (currentTotal * 3, (sprintf "%dを積算" 3) :: history))
    return "完了!"
  }

 ついでに、行の横幅が若干短くなるので、sprintfも適用してしまいましょう。

let calculatorActions =
  state {
    do! StatefulFunc(fun (currentTotal, history) -> (), (currentTotal + 2, "2を加算" :: history))
    do! StatefulFunc(fun (currentTotal, history) -> (), (currentTotal * 3, "3を積算" :: history))
    return "完了!"
  }

2.ワークフロー展開

 どんどん進めましょう。次は、do!やreturnをstate.Bindやstate.Returnに置き換えていく作業です。変換ルールは以下のようなものでしたね。
return exp          ⇒      b.Return(exp)
do! expr in expr2   ⇒      b.Bind(expr, fun() -> expr2)

 この変換を施したものが下のソースです。次の変換が楽になるよう、Bindの二つの引数は字下げしてその下に縦に並べるようにしました。

let calculatorActions =
  state.Bind(
    StatefulFunc(fun (currentTotal, history) -> (), (currentTotal + 2, "2を加算" :: history)),
    (fun () -> 
      state.Bind(
        StatefulFunc(fun (currentTotal, history) -> (), (currentTotal * 3, "3を積算" :: history)),
        (fun () -> state.Return("完了!"))
      )
    )
  )

 ここまでできたら、実行して動きが変わらないことを確認してください。

3.ワークフロー定義展開

 さて、後半のヤマ場です。ここで式の大きさが最大になります。
 まずstate.Returnの展開から手をつけましょう。
member this.Return(x : 'a) =
  StatefulFunc(fun initialState -> x, initialState)
 ですから、xは"完了"という文字列になりますね。

let calculatorActions =
  state.Bind(
    StatefulFunc(fun (currentTotal, history) -> (), (currentTotal + 2, "2を加算" :: history)),
    (fun () -> 
      state.Bind(
        StatefulFunc(fun (currentTotal, history) -> (), (currentTotal * 3, "3を積算" :: history)),
        (fun () -> StatefulFunc(fun initialState -> "完了!", initialState))
      )
    )
  )

 次に、内側のstate.Bindを展開します。Bindの定義は以下のようなものでした。

member this.Bind(
                  result : StatefulFunc<'state, 'a>,
                  restOfComputation : 'a -> StatefulFunc<'state, 'b>
                ) =
  StatefulFunc(fun initialState ->
    let result, updatedState = Run result initialState
    Run (restOfComputation result) updatedState
  )
 Bindの二つの引数を、StatefulFuncの中のresult(1行目の後ろに出てくる方)とrestOfComputationの代わりに置き換える作業を注意深く行います。
let calculatorActions =
  state.Bind(
    StatefulFunc(fun (currentTotal, history) -> (), (currentTotal + 2, "2を加算" :: history)),
    (fun () -> 
      StatefulFunc(fun initialState ->
        let result, updatedState = 
          Run (StatefulFunc(fun (currentTotal, history) -> (), (currentTotal * 3, "3を積算" :: history))) initialState
        Run ((fun () -> StatefulFunc(fun initialState -> "完了!", initialState)) result) updatedState
      )
    )
  )
 横に長くなったので、行の途中に改行を入れました。
 さらに、最初のstate.Bindを置き換えます。
let calculatorActions =
  StatefulFunc(fun initialState ->
    let result, updatedState = 
      Run (StatefulFunc(fun (currentTotal, history) -> (), (currentTotal + 2, "2を加算" :: history))) initialState
    Run (
      (fun () -> 
        StatefulFunc(fun initialState ->
          let result, updatedState = 
            Run (StatefulFunc(fun (currentTotal, history) -> (), (currentTotal * 3, "3を積算" :: history))) initialState
          Run ((fun () -> StatefulFunc(fun initialState -> "完了!", initialState)) result) updatedState
        )
      ) result
    ) updatedState
  )
 横幅を詰めるため改行を入れていますが、関数の構造を見失わないように注意してください。
 これで、StateBuilderとはオサラバです。

4.戻りは下り坂を駆け下りるように簡約(1段目)

 ここで使用できる簡約のテクニックは、Add展開編で使用したものと全く同じです。簡約も内側から、前からが基本ですので、一番内側のStatefulFuncの1行目、"3を積算"がある行のRunをはずすところから始めます。

let calculatorActions =
  StatefulFunc(fun initialState ->
    let result, updatedState = 
      Run (StatefulFunc(fun (currentTotal, history) -> (), (currentTotal + 2, "2を加算" :: history))) initialState
    Run (
      (fun () -> 
        StatefulFunc(fun initialState ->
          let result, updatedState = 
            (fun (currentTotal, history) -> (), (currentTotal * 3, "3を積算" :: history)) initialState
          Run ((fun () -> StatefulFunc(fun initialState -> "完了!", initialState)) result) updatedState
        )
      ) result
    ) updatedState
  )
 ここまで進めると、その行のinitialStateは(currentTotal, history)という何かのタプルであることがはっきりするので、仮引数のinitialStateもろとも(currentTotal, history)で置き換えられます。
let calculatorActions =
  StatefulFunc(fun initialState ->
    let result, updatedState = 
      Run (StatefulFunc(fun (currentTotal, history) -> (), (currentTotal + 2, "2を加算" :: history))) initialState
    Run (
      (fun () -> 
        StatefulFunc(fun (currentTotal, history) ->
          let result, updatedState = 
            (fun (currentTotal, history) -> (), (currentTotal * 3, "3を積算" :: history)) (currentTotal, history)
          Run ((fun () -> StatefulFunc(fun initialState -> "完了!", initialState)) result) updatedState
        )
      ) result
    ) updatedState
  )
 ちなみに、このように、仮引数もろとも束縛変数の名前を都合のいい名前に一気に置き換えてしまうことを「α-変換(アルファへんかん)」と言います。文系の大学行ってた僕は、なんかこの響きを「かっこいい・・(萌)」などと感じてしまうのですが、同時に世代的にどうしても「ドラエ問題」を思い出してしまいます。蛇足ですが。
 こうして名前を変えた(currentTotal, history)(元initialState)を、その前の関数に適用します。同じ名前の変数を適用しているので、中身を変える必要はありません。
let calculatorActions =
  StatefulFunc(fun initialState ->
    let result, updatedState = 
      Run (StatefulFunc(fun (currentTotal, history) -> (), (currentTotal + 2, "2を加算" :: history))) initialState
    Run (
      (fun () -> 
        StatefulFunc(fun (currentTotal, history) ->
          let result, updatedState = (), (currentTotal * 3, "3を積算" :: history)
          Run ((fun () -> StatefulFunc(fun initialState -> "完了!", initialState)) result) updatedState
        )
      ) result
    ) updatedState
  )
 もう一つ、関数を特定の値に適用して式を整理することを「β-簡約(ベータかんやく)」と言います(萌)。こんな当たり前のことにわざわざこんな小難しい名前がついているのは、α-変換とβ-簡約の二つでほとんどの式変形ができてしまうからなのですが、その辺の理屈は「ラムダ計算」というキーワードで色々調べてみてください。
 さて、今のβ-簡約でその行の右にあったresult、updateStateが何であるかが分かりましたので、それを2行目のresult、updatedStateと置き換えます。
let calculatorActions =
  StatefulFunc(fun initialState ->
    let result, updatedState = 
      Run (StatefulFunc(fun (currentTotal, history) -> (), (currentTotal + 2, "2を加算" :: history))) initialState
    Run (
      (fun () -> 
        StatefulFunc(fun (currentTotal, history) ->
          Run (
            (fun () -> StatefulFunc(fun initialState -> "完了!", initialState)) ()
          ) (currentTotal * 3, "3を積算" :: history)
        )
      ) result
    ) updatedState
  )
 2行目のfun ()を()に適用したのち、RunでStatefulFuncの中身を取り出して元updatedStateだった値に適用するところまで一気にいきます。
let calculatorActions =
  StatefulFunc(fun initialState ->
    let result, updatedState = 
      Run (StatefulFunc(fun (currentTotal, history) -> (), (currentTotal + 2, "2を加算" :: history))) initialState
    Run (
      (fun () -> 
        StatefulFunc(fun (currentTotal, history) -> "完了!", (currentTotal * 3, "3を積算" :: history))
      ) result
    ) updatedState
  )

5.そろそろフィニッシュ(2段目)

 同じような「Run適用(実は端折ってますがこれはRunの展開+β-簡約です)⇒α-変換(currentTotal, history)⇒β-簡約」の操作は、"2を加算"の行にもできます。もうあんまり頭を使わなくなってきませんか?そもそもこれはコンピュータがやっている作業ですので、機械的な作業になるのは当たり前なのです。
let calculatorActions =
  StatefulFunc(fun (currentTotal, history) ->
    let result, updatedState = (), (currentTotal + 2, "2を加算" :: history)
    Run (
      (fun () -> 
        StatefulFunc(fun (currentTotal, history) -> "完了!", (currentTotal * 3, "3を積算" :: history))
      ) result
    ) updatedState
  )
 これによってまたresultとupdatedStateが確定しましたので、後に出てくるresultとupdatedStateを置き換えます。
let calculatorActions =
  StatefulFunc(fun (currentTotal, history) ->
    Run (
      (fun () -> 
        StatefulFunc(fun (currentTotal, history) -> "完了!", (currentTotal * 3, "3を積算" :: history))
      ) ()
    ) (currentTotal + 2, "2を加算" :: history)
  )
 fun ()->の関数をβ-簡約します。2行削除するだけです。
let calculatorActions =
  StatefulFunc(fun (currentTotal, history) ->
    Run (
      StatefulFunc(fun (currentTotal, history) -> "完了!", (currentTotal * 3, "3を積算" :: history))
    ) (currentTotal + 2, "2を加算" :: history)
  )
 Runを展開してβ-簡約します。
let calculatorActions =
  StatefulFunc(fun (currentTotal, history) ->
    (fun (currentTotal, history) -> 
      "完了!", (currentTotal * 3, "3を積算" :: history)
    ) (currentTotal + 2, "2を加算" :: history)
  )
 勢いよく走っていたのにここで止めたのは、ちょっと注意が必要だからです。残った式にはcurrentTotal、historyという変数が出てきていますが、2行目のfunの束縛変数である(currentTotal, history)と、3行目のfunの束縛変数(currentTotal, history)とは別物ですのでごっちゃにしないようにしてください。必要はないのですが、違いが分かりやすいように一度α-変換(萌)をしておきましょう。
let calculatorActions =
  StatefulFunc(fun (currentTotal, history) ->
    (fun (currentTotal2, history2) -> 
      "完了!", (currentTotal2 * 3, "3を積算" :: history2)
    ) (currentTotal + 2, "2を加算" :: history)
  )
 内側の束縛変数の名前を変えました。これでもう混ざりませんね。最後のβ-簡約をしましょう。
let calculatorActions =
  StatefulFunc(fun (currentTotal, history) -> "完了!", ((currentTotal + 2) * 3, "3を積算" :: ("2を加算" :: history)))
 お疲れ様でした。これがcalculatorActionsの正体です。

6.実行

 サンプルプログラムの実行では、以下のようなコードを実行していました。
let res, stat = Run calculatorActions (0, [])
 もうわかると思いますが、これは先ほどできた式を使ってこのように展開されます。
let res, stat = 
  Run (
    StatefulFunc(fun (currentTotal, history) -> "完了!", ((currentTotal + 2) * 3, "3を積算" :: ("2を加算" :: history)))
  ) (0, [])
 ここからRunとStatefulFunをはずします。
let res, stat = 
  (fun (currentTotal, history) -> "完了!", ((currentTotal + 2) * 3, "3を積算" :: ("2を加算" :: history))) (0, [])
 β-簡約します。
let res, stat = "完了!", ((0 + 2) * 3, "3を積算" :: ("2を加算" :: []))
 どのように実行結果が確定したかが、ここからそのまま読み取れると思います。
val stat : int * string list = (6, ["3を積算"; "2を加算"])
val res : string = "完了!"

7.結局StateBuilderとは何なのか

 このような面倒な式変形の結果、劇的にソースが短くなるなら、「最初からそう書けばいいじゃないか」という気持ちが沸々と湧き上がってくるかもしれません。僕は少なくとも最初そんな風に感じていました。
 ワークフローのルールに従って簡単だった式を一度煩雑な式に展開したあと、それをわざわざまた簡約して、それなら最初から簡約したものを書いておけば済むことです。なぜそうしないでワークフローを使うのでしょうか。

プログラムソースの清濁分離

 一つの目的は、ソースから見かけ上煩雑な処理を振るいにかけ、本質的なことだけを記述できるようにすることです。通常プログラムには、業務のために行っていることと、そのコンピュータ自身が業務を続けるために行っていることがあり、多くの場合その両方が交互に出てきます。業務処理とエラー処理などがそれにあたります。こうしたプログラムでは、大切な業務処理が、ひどいときはそれよりずっと長いエラー処理のコードの隙間に入っているような状態になり、本質的な処理を見抜きにくいプログラムになりがちです。ワークフローは、そのような本質的な処理の間に混じる瑣末な汚れ仕事をビルダークラスの中に閉じ込めてしまうことができます。こうすることで、業務を記述する人は、業務内容をすっきりとソースに記述できるようになるのです。このあたりは、オブジェクト指向のフレームワークと似たところがありますが、そのような要求に対し、ワークフローは異なるアプローチを提供していると考えていいと思います。

そもそもそれはコンピュータの仕事だ

 関数型言語では、上でやっているような関数の適用と簡約こそが「演算」そのものであって、それを人間でなくコンピュータにやらせるのは自然なことです。それなのに、多少処理が早いなどといった理由で「前もって人間が展開した式を書いておけばよい」というのは、ソースの中の「PI * 2」という記述を6.28...とわざわざ人間が計算した値を書いておいたほうがいいという主張と同じです。ソースの書き方で実行速度が変わるのは事実ですが、そもそも人間にとって分かりやすく書かれたものを、翻訳の手間をかけて実行するのはコンピュータの仕事ですし、それに耐えるだけの性能を今のコンピュータは十分持っているのです。犬ぞりを人間が押してやることに意味はあるかもしれませんが、ガソリンの入ったフェラーリは乗って運転してこそ意味を発揮するのです。

実行順の保障

 StatefulFuncのような関数ではあまり恩恵を感じませんが、ワークフローというのはその名前の通り、実行順を保障する重要な機能があります。展開の途中で(fun () -> ~)という関数をunit値()に適用する処理がありましたが、一見意味のなさそうなこういう処理が、プログラムの実行順を保障してくれています。

ワークフロー(モナド)とどう付き合っていくか

 人が書いたワークフローの動きを追っていくだけでもこれほど大変なのですから、自分でビルダークラスを書き、その上で動く関数を設計して業務を記述できるよううになるところまで行くことはなかなか並大抵ではなさそうです。
 ただ一つ言える事は、これからもし関数型言語が普及していくなら、プログラマには「自分でワークフロー(モナド)が設計できる」プログラマと「人が作ったワークフローを使うだけ」のプログラマに分かれていくことになります。オブジェクト指向言語で言うなら、フレームワークを作れるプログラマと、フレームワークを使うだけのプログラマの違いです。
 どうせ今関数型言語を勉強するなら、ワークフロー(モナド)が設計できるプログラマを目指すべきではないかと思うのです。

(文責:片山 功士  2011/12/29)



今日: -
昨日: -
トータル: -
最終更新:2012年02月03日 01:57