StatefulFuncはなぜ動くか for展開編(オマケ)


1.forを使ったループの導入

 StateBuilderにはForメソッドがきちんと実装されており、計算式の中にforループを記述することができます。試してみましょう。

let calculatorActions =
  state {
    for i = 1 to 5 do
      do! Add i
    do! Multiply 3
  }

let res, stat = Run calculatorActions (0, [])

これを実行すると、以下のような実行結果が得られます。

val calculatorActions : StatefulFunc<(int * string list),unit> =
  StatefulFunc <fun:Delay@184-2>
val stat : int * string list =
  (45, ["3を乗算"; "5を加算"; "4を加算"; "3を加算"; "2を加算"; "1を加算"])
val res : unit = ()
素晴らしいほど期待通りですね。今回はこれがどのように展開されているかを見ていきましょう。


2.Add、Multiplyの展開

 calculatorActions展開編と同様に、AddとMultiplyを「Add展開編」で出来上がった最後の式を使い展開します。ついでに加算には無理ですが積算のsprintfを適用しましょう。
let calculatorActions =
  state {
    for i = 1 to 5 do
      do! StatefulFunc(fun (currentTotal, history) -> (), (currentTotal + i, (sprintf "%dを加算" i) :: history))
    do! StatefulFunc(fun (currentTotal, history) -> (), (currentTotal * 3, "3を積算" :: history))
  }

3.return()の補完

 今回は式がdo!で終わっているので、return ()を最後に補完します。また、実はforループの末尾にもreturn ()を補完しないとここから先の展開ができません。

let calculatorActions =
  state {
    for i = 1 to 5 do
      do! StatefulFunc(fun (currentTotal, history) -> (), (currentTotal + i, (sprintf "%dを加算" i) :: history))
      return ()
    do! StatefulFunc(fun (currentTotal, history) -> (), (currentTotal * 3, "3を積算" :: history))
    return ()
  }
 ここまでやったら、一度実行して結果が変わっていないことを確認しましょう。

4.ワークフロー展開

 とりあえず頭を使わないで、returnの展開をします。ここまでは余裕ですね。コンパイル通りませんが。
let calculatorActions =
  state {
    for i = 1 to 5 do
      do! StatefulFunc(fun (currentTotal, history) -> (), (currentTotal + i, (sprintf "%dを加算" i) :: history))
      state.Return ()
    do! StatefulFunc(fun (currentTotal, history) -> (), (currentTotal * 3, "3を積算" :: history))
    state.Return ()
  }
 続いて、二つのdo!を一気に展開します。
let calculatorActions =
  state {
    for i = 1 to 5 do
      state.Bind(
        StatefulFunc(fun (currentTotal, history) -> 
          (), (currentTotal + i, (sprintf "%dを加算" i) :: history)),
        (fun () -> state.Return ())
      )
    state.Bind(
      StatefulFunc(fun (currentTotal, history) -> 
        (), (currentTotal * 3, "3を積算" :: history)),
      (fun () -> state.Return ())
    )
  }
 行が横に長くなるとWiki的に支障が出るため、fun (currentTotal, history) ->の右に改行を入れていますが、実際に作業をする場合はこんなところに改行を入れないほうが作業を安全に進められると思います。
 そしてforの展開をします。初めてですので展開ルールを見てみましょう。「実践F#関数型プログラミング入門」に記述がありますが、どう考えても間違っていて、以下のルールが正しいと思います。
for ident = expr1 to expr2 do expr3    ⇒     b.For(seq{expr1..expr2}, fun ident -> expr3)
 さすがに要素が多いですが、ひるまずに機械的に作業をします。
let calculatorActions =
  state {
    state.For(seq{1..5}, fun i ->
      state.Bind(
        StatefulFunc(fun (currentTotal, history) -> 
          (), (currentTotal + i, (sprintf "%dを加算" i) :: history)),
        (fun () -> state.Return ())
      )
    )
    state.Bind(
      StatefulFunc(fun (currentTotal, history) -> 
        (), (currentTotal * 3, "3を積算" :: history)),
      (fun () -> state.Return ())
    )
  }
 最後に、これは結構伏兵なのですが、forのブロックとそれ以降をつなぐ作業が必要になります。二つの式を遷移的に(前の作業が終わってから次をやるように)つなげる再は、Combineを使います。
trans-cexpr0; cexpr1     ⇒    b.Combine(trans-cexpr0, cexpr1)
 Bindを少し簡単にしたような感じでしょうか。

let calculatorActions =
  state.Combine(
    state.For(seq{1..5}, fun i ->
      state.Bind(
        StatefulFunc(fun (currentTotal, history) -> 
          (), (currentTotal + i, (sprintf "%dを加算" i) :: history)),
        (fun () -> state.Return ())
      )
    ),
    state.Bind(
      StatefulFunc(fun (currentTotal, history) -> 
        (), (currentTotal * 3, "3を積算" :: history)),
      (fun () -> state.Return ())
    )
  )

これでひと段落です。コンパイルが通り、実行結果が変わらないことを確認します。

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

 CombineとForが一つずつ、BindとReturnが二つずつありますが、逃げないで!
 とりあえず、Returnの展開。
let calculatorActions =
  state.Combine(
    state.For(seq{1..5}, fun i ->
      state.Bind(
        StatefulFunc(fun (currentTotal, history) -> 
          (), (currentTotal + i, (sprintf "%dを加算" i) :: history)),
        (fun () -> StatefulFunc(fun initialState -> (), initialState))
      )
    ),
    state.Bind(
      StatefulFunc(fun (currentTotal, history) -> 
        (), (currentTotal * 3, "3を積算" :: history)),
      (fun () -> StatefulFunc(fun initialState -> (), initialState))
    )
  )
 その勢いで二つあるBindの展開。さすがに慣れてきました。
let calculatorActions =
  state.Combine(
    state.For(seq{1..5}, fun i ->
      StatefulFunc(fun initialState ->
        let result, updatedState = 
          Run (StatefulFunc(fun (currentTotal, history) -> 
            (), (currentTotal + i, (sprintf "%dを加算" i) :: history))) initialState
        Run ((fun () -> StatefulFunc(fun initialState -> 
          (), initialState)) result) updatedState
      )
    ),
    StatefulFunc(fun initialState ->
      let result, updatedState = 
        Run (StatefulFunc(fun (currentTotal, history) -> 
          (), (currentTotal * 3, "3を積算" :: history))) initialState
      Run ((fun () -> StatefulFunc(fun initialState -> 
        (), initialState)) result) updatedState
    )
  )
 内側から、と言うことでForの展開。state.Forの関数定義は以下のようなものです。
member this.For(
                  elements : seq<'a>,
                  forBody : ('a -> StatefulFunc<'state, unit>)
               ) =
  StatefulFunc(fun initialState ->
    let state = ref initialState
    for e in elements do
      let (), updatedState = Run (forBody e) (!state)
      state := updatedState
    (), !state
  )
 さすがに長いですが、一つ目のシーケンスはseq{1..5}ですので、残り全部がforBodyです。
let calculatorActions =
  state.Combine(
    StatefulFunc(fun initialState ->
      let state = ref initialState
      for e in seq{1..5} do
        let (), updatedState = 
          Run (
            (fun i ->
              StatefulFunc(fun initialState ->
                let result, updatedState = 
                  Run (StatefulFunc(fun (currentTotal, history) -> 
                    (), (currentTotal + i, (sprintf "%dを加算" i) :: history))) initialState
                Run ((fun () -> StatefulFunc(fun initialState -> 
                  (), initialState)) result) updatedState
              )
            )
          e) (!state)
        state := updatedState
      (), !state
    ),
    StatefulFunc(fun initialState ->
      let result, updatedState = 
        Run (StatefulFunc(fun (currentTotal, history) -> 
          (), (currentTotal * 3, "3を積算" :: history))) initialState
      Run ((fun () -> StatefulFunc(fun initialState -> 
        (), initialState)) result) updatedState
    )
  )
 再び悪夢のようになってきましたが、またCombileの展開が残っています。
member this.Combine(
                     partOne : StatefulFunc<'state, unit>,
                     partTwo : StatefulFunc<'state, 'a>
                   ) =
  StatefulFunc(fun initialState ->
    let (), updatedState = Run partOne initialState
    Run partTwo updatedState
  )
 ほとんどBindと同じですね。二つ目の引数が関数でなくそのままStatefulFuncになっているぐらいです。
let calculatorActions =
  StatefulFunc(fun initialState ->
    let (), updatedState = 
      Run (
        StatefulFunc(fun initialState ->
          let state = ref initialState
          for e in seq{1..5} do
            let (), updatedState = 
              Run (
                (fun i ->
                  StatefulFunc(fun initialState ->
                    let result, updatedState = 
                      Run (StatefulFunc(fun (currentTotal, history) -> 
                        (), (currentTotal + i, (sprintf "%dを加算" i) :: history))) initialState
                    Run ((fun () -> StatefulFunc(fun initialState -> 
                      (), initialState)) result) updatedState
                  )
                )
              e) (!state)
            state := updatedState
          (), !state
        )
      ) initialState
    Run (
      StatefulFunc(fun initialState ->
        let result, updatedState = 
          Run (StatefulFunc(fun (currentTotal, history) -> 
            (), (currentTotal * 3, "3を積算" :: history))) initialState
        Run ((fun () -> StatefulFunc(fun initialState -> 
          (), initialState)) result) updatedState
      )
    ) updatedState
  )
 なんだかマトリックスというか攻殻機動隊のオープニングを見ているような空気になってきました。もうこうなるとコードを見て合っているかどうか判定するのは不可能に近いですが、実際に実行してみて結果が変わらないことだけを心の支えに、これから簡約の作業に入っていきます。ここから下り坂です。そう思うと上のコードもどこか富士山のようなピラミッドのような感じにも見えてきます・・・・かね。

β-簡約!α-変換!β-簡約!

 もうどこから手をつけていいかわからないぐらいですが、内側から、前からの原則を考えながらRun適用(β-簡約)⇒α-変換⇒β-簡約を繰り返します。もういちいち流れを書いていられないので、一渡り作業するごとにソースの移り変わりを見ていただきます。
let calculatorActions =
  StatefulFunc(fun initialState ->
    let (), updatedState = 
      Run (
        StatefulFunc(fun initialState ->
          let state = ref initialState
          for e in seq{1..5} do
            let (), updatedState = 
              Run (
                (fun i ->
                  StatefulFunc(fun (currentTotal, history) ->
                    (),  (currentTotal + i, (sprintf "%dを加算" i) :: history)
                  )
                )
              e) (!state)
            state := updatedState
          (), !state
        )
      ) initialState
    Run (
      StatefulFunc(fun initialState ->
        let result, updatedState = 
          Run (StatefulFunc(fun (currentTotal, history) -> (), (currentTotal * 3, "3を積算" :: history))) initialState
        Run ((fun () -> StatefulFunc(fun initialState -> (), initialState)) result) updatedState
      )
    ) updatedState
  )

 forの中を一通り簡約してみました。ここから、iにeを設定してその外のRunを解いていきます。 
let calculatorActions =
  StatefulFunc(fun initialState ->
    let (), updatedState = 
      Run (
        StatefulFunc(fun initialState ->
          let state = ref initialState
          for e in seq{1..5} do
            let (), updatedState = 
              (fun (currentTotal, history) -> (),  (currentTotal + e, (sprintf "%dを加算" e) :: history)) (!state)
            state := updatedState
          (), !state
        )
      ) initialState
    Run (
      StatefulFunc(fun initialState ->
        let result, updatedState = 
          Run (StatefulFunc(fun (currentTotal, history) -> (), (currentTotal * 3, "3を積算" :: history))) initialState
        Run ((fun () -> StatefulFunc(fun initialState -> (), initialState)) result) updatedState
      )
    ) updatedState
  )
 さて、これからどうしましょうか。とりあえずfunの引数に何が入っても、戻り値のタプルの最初は()になりますからこれは取っても差し支えないでしょう。
 また(currentTotal, history)のα-変換の相手が参照型で、そのままではうまくいかないため、一度letで束縛するように変えてみます。
let calculatorActions =
  StatefulFunc(fun initialState ->
    let (), updatedState = 
      Run (
        StatefulFunc(fun initialState ->
          let state = ref initialState
          for e in seq{1..5} do
            let (currentTotal, history) = !state
            state := (currentTotal + e, (sprintf "%dを加算" e) :: history)
          (), !state
        )
      ) initialState
    Run (
      StatefulFunc(fun initialState ->
        let result, updatedState = 
          Run (StatefulFunc(fun (currentTotal, history) -> (), (currentTotal * 3, "3を積算" :: history))) initialState
        Run ((fun () -> StatefulFunc(fun initialState -> (), initialState)) result) updatedState
      )
    ) updatedState
  )
 だいぶすっきりしましたが、forの中身はもうこれ以上どうもならなそうです。後半に着手しましょう。もう怖いものなくなってきました。
let calculatorActions =
  StatefulFunc(fun initialState ->
    let (), updatedState = 
      Run (
        StatefulFunc(fun initialState ->
          let state = ref initialState
          for e in seq{1..5} do
            let (currentTotal, history) = !state
            state := (currentTotal + e, (sprintf "%dを加算" e) :: history)
          (), !state
        )
      ) initialState
    (fun (currentTotal, history) -> (), (currentTotal * 3, "3を積算" :: history)) updatedState
  )
 forの外側を覆っているRunを取り除きましょう。その他、無駄な()の受け渡しを取ったり、色々やって出てきたのが下の式です。
let calculatorActions =
  StatefulFunc(fun initialState ->
    let (currentTotal, history) = 
      let state = ref initialState
      for e in seq{1..5} do
        let (currentTotal, history) = !state
        state := (currentTotal + e, (sprintf "%dを加算" e) :: history)
      !state
    (), (currentTotal * 3, "3を積算" :: history)
  )
 ここから先にさらに何かやるとしたら、3行目のletを!stateの左に持ってきて、
let calculatorActions =
  StatefulFunc(fun initialState ->
    let state = ref initialState
    for e in seq{1..5} do
      let (currentTotal, history) = !state
      state := (currentTotal + e, (sprintf "%dを加算" e) :: history)
    let (currentTotal, history) = !state
    (), (currentTotal * 3, "3を積算" :: history)
  )
 としても結果は変わらないと思うのですが、この二つが等価な式であると言い切る自信が僕にはいまひとつありません。下のほうが字下げがひとつ減って分かりやすくなっていると思うのですがどんなもんでしょうか。
 実行結果も展開前と変わらず、作業は成功しているようです。

できた式を見てみると

 このドタバタの結果できた式を見ると、結構当たり前のことをしているだけのように思えます。状態をリファレンスに閉じ込めて可変な値とし、ループ中に状態を失わないようになっているところが憎いですね。
 しかしあの富士山が結果的にほとんど無駄のないコードになってしまったのはなかなかドラマチックな気さえします。これを読んでいる方は、ぜひぜひ、ただこれを読んでいるだけでなく、自分の手を動かしてこのワークフロー展開を体験してみてください。ワークフローに対する理解が劇的に深まるだけでなく、「ラムダ式どんとこい」的な基礎体力増強も期待できます。
 ちなみに筆者は、「プログラミング言語の基礎概念(サイエンス社)」というテキストのオンライン練習問題をやることで、こういう巨大式に対する耐性を身に付けた気がします。時間がある人は決して損をさせませんからこちらにも挑戦してください。というようなアフェリエイトな流れになりつつも、そのへんよく分からないのでただ紹介するだけだったり。

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


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