StatefulFuncはなぜ動くか while展開編(オマケ)
1.whileを使ったループの導入
StateBuilderにはWhileメソッドも実装されており、計算式の中にwhileループを記述できます。whileループはその構造上破壊的な代入が伴うのであまり関数型プログラミングで使われることがありませんが、せっかくなのでこれも試してみましょう。などと調子よく書き始めてから色々試したのですが、想像以上にイバラの道でした。ほとんど参考になる情報がなく、あっても間違っていたりで何日も悩まされました。
そんなこんなで書いたのがこのclaculatorActionsです。
let GetTotal = StatefulFunc (fun (total, history) -> total, (total, history))
let calculatorActions =
state {
do! Add 3
let! total = GetTotal
let stat = ref total
while (!stat > 0) do
do! Subtract 1
let! total = GetTotal
stat := total
return "End"
}
let res, stat = Run calculatorActions (0, [])
これを実行すると、以下のような実行結果が得られます。
val calculatorActions : StatefulFunc<(int * string list),string> =
StatefulFunc <fun:Delay@184>
val stat : int * string list = (0, ["1を減算"; "1を減算"; "1を減算"; "3を加算"])
val res : string = "End"
最初に3を加算し、1を引きながらループを回って0になったら抜ける、という処理になっております。たったこれだけなのに、意外にごちゃごちゃしてしまいました。let!でそのまま参照変数を破壊的に変更できないからなのですが、このソースを見ただけでも、計算式でwhileループが積極的に使われない理由がわかるというものです。ただ、ループの中で外からの副作用を受け、その結果を見てループするようなケースではforループでは処理できませんから、whileも覚えておくに越したことはないでしょう。
2.GetTotalの意味とcalculatorActionsの解説
これまでの流れでは、「じゃ展開」ということになるのでしょうが、whileループについてはいろいろと説明しておかないといけないことがあるので、それを説明します。
まず、GetTotalという関数ですが、これは机の下でやり取りされている現在の計算結果を水面上に出してくる関数です。「机の下って何の話?」という人は、
StatefulFuncの意味的理解を先に読んでください。ここに出てくる、GetState2の名前をGetTotalにしただけのものです。GetStateをそのまま使い、historyを捨ててしまってもいいのですが、ソースが余計にごちゃごちゃするので作りました。
さて、calculatorActionsですが、do! Add 3はいいとして、その次の行です。
ここで、さっきのGetTotalを使い、現在保持している計算結果をtotalに束縛しています。そして、その次の行で、その結果をrefでくるんでstatに再度束縛しています。
このあたり1行でさらっと書ければいいのですが、僕の知ってる範囲ではできませんでした。
whileの条件式の中でそのstatを参照し、ループに入ります。先頭で1を引いて、再度Totalを取得し、statに再代入しています。
とまぁこれだけといえばこれだけなのですが、ここまでたどり着くのになんで何日も苦労したんだか。
3.Whileメソッドの実装を確認
さて、これから展開に入っていくのですが、その前にWhileメソッドの実装を見ておきましょう。
member this.While(
predicate : unit -> bool,
body : StatefulFunc<'state, unit>
) =
StatefulFunc(fun initialState ->
let state = ref initialState
while predicate() = true do
let (), updatedState = Run body (!state)
state := updatedState
(), !state
)
これを見ると、Whileはunit->boolの関数predicateとStatefulFuncオブジェクトのbodyのタプルを引数にとり、「predicateがtrueの間だけbodyを評価し続け、最後に評価したbodyの結果を返す」という実装になっていることがわかります。ループを回っている間の状態の変化はstateが保持していますが、predicateは外から与えられた関数であるためこのstateを直接参照することができません。このため、GetState関数を使って机の下のstateを机の上に出しているのです。
4.展開ルールの確認
ここからは結構やっかいです。上の式の展開にはWhileメソッドのほかにDelay、Combine、Zeroメソッドへを使ったルールが必要となってきます。
しかし何よりWhileの展開で一番困ったのは、いろいろなところに書いてあるWhile展開のルールが、英語の情報ソースも含め、一次ソースと思われる「
The F# 2.0 Language Specification」以外ことごとく間違っているか情報不足であることです。特に
英語msdnの間違いと情報不足はちょっと許しがたいレベルで、おそらくそれに間違い/不足があるせいでほかのソースが共倒れしていると思われます。この英語msdnが一番まずいのは、その間違い/不足が2011年8月11日にコメントで示されているにもかかわらず9か月以上放置されていることです。
気を取り直して英語msdnに書かれている展開ルールを見てみましょう。
{| while expr do cexpr |} ⇒ builder.While(fun () -> expr), builder.Delay({|cexpr |})
ところが、「The F# 2.0 Language Specification」には
{| while expr do cexpr |}C = b.While((fun () -> expr), {| cexpr |}Delayed)
とあり、その後の
{| cexpr |}Delayed is b.Delay(fun () -> {| cexpr |}C).
という記述を合わせ解釈すると、正しくは
{| while expr do cexpr |} ⇒ builder.While(fun () -> expr), builder.Delay(fun () -> {|cexpr |})
であることがわかります。Delayの中がfun ()->となっているかどうかのわずかな違いですが、コンパイラは型違いをどこか別のところで辻褄合わせしようとするのでエラーの理由がわかりにくいのです。最近はやっと冷静に対処できるようになってきましたが、計算式まわりで型エラーが出たら最初は投げ出していました。
Combineの記述にも間違いがあり、
{| cexpr1; cexpr2 |} ⇒ builder.Combine({|cexpr1 |}, {| cexpr2 |})
とありますが実は
{| trans-cexpr0; cexpr1 |} = b.Combine({| trans-cexpr0 |}C, {| cexpr1 |}Delayed)
であり、上のDelayedの定義から
{| cexpr1; cexpr2 |} ⇒ builder.Combine({|cexpr1 |}, builder.Delay(fun() -> {| cexpr2 |}))
が正しいことがわかります。
この間違いは日本語のmsdnにも引き継がれていて、しかもコメントが無いためこれだけで間違いに気づくのは難しいでしょう。「実践」は「The F# 2.0 Language Specification」を参照して書かれているようでこのような間違いが無いのですが、Delayedの説明が無いのが残念です。
ここで突然出てきているtrans-cexpr0というのは、計算式(Computation Expression)の式のうち、最後のルール「{| other-expr |} ⇒ expr; builder.Zero()」以外で生成されたものを意味しています。細かいことですが、英語の「The F# 2.0 Language Specification」以外で記述を見ないので一応書いておきます。
いろいろ出てきましたが、ここで紹介した以下の展開ルールが、今回出てきたalculatorActionsの展開に必要なルールのすべてです。
{| while expr do cexpr |} ⇒ builder.While(fun () -> expr), builder.Delay(fun () -> {|cexpr |})
{| cexpr1; cexpr2 |} ⇒ builder.Combine({|cexpr1 |}, builder.Delay(fun() -> {| cexpr2 |}))
{| other-expr |} ⇒ expr; builder.Zero()
これに、これまでやったlet!の展開、do!の展開、Return ()の補完を思い出してもらえば、今回の展開はできるはずです。
蛇足ですが、このReturnの補完は、次のように記述されているべきなのではないかと思います。おそらく単なる記述漏れなのではないかと。そう考えると、これまで「補完ルール」と呼んでいいたものは単なる計算式の展開ルールだったということになります。
{| do! expr |} ⇒ builder.Bind(expr, (fun () -> builder.Return ()))
というわけで、展開を始めましょう。
5.それでは展開
とはいえ、下の計算式を見ると、これがfor展開編以上に長い旅になることは火を見るよりも明らかなように思えます。「あとは各自トライしてみてくださいね!」で済まそうかとも思ったのですが・・・少なくとも糖衣構文の展開までは実際にやってみようかと思います。
let calculatorActions =
state {
do! Add 3
let! total = GetTotal
let stat = ref total
while (!stat > 0) do
do! Subtract 1
let! total = GetTotal
stat := total
return "End"
}
まず、whileブロックの末尾にあるstat := totalから手を付けます。これは単なる式なので、{| other-expr |} ⇒ expr; builder.Zero()を使って以下のようにします。ついでの最後のreturnも展開しておきましょう。
let calculatorActions =
state {
do! Add 3
let! total = GetTotal
let stat = ref total
while (!stat > 0) do
do! Subtract 1
let! total = GetTotal
(stat := total; state.Zero())
state.Return "End"
}
次に、その行とその前の行のlet!をBindに展開します。
let calculatorActions =
state {
do! Add 3
let! total = GetTotal
let stat = ref total
while (!stat > 0) do
do! Subtract 1
state.Bind(
GetTotal,
fun total ->
(stat := total; state.Zero())
)
state.Return "End"
}
さらにその前のdo!と合わせてBindに展開します。
let calculatorActions =
state {
do! Add 3
let! total = GetTotal
let stat = ref total
while (!stat > 0) do
state.Bind(
Subtract 1,
fun () ->
state.Bind(
GetTotal,
fun total ->
(stat := total; state.Zero())
)
)
state.Return "End"
}
いよいよwhileを展開します。
let calculatorActions =
state {
do! Add 3
let! total = GetTotal
let stat = ref total
state.While(
(fun () -> !stat > 0),
state.Delay(
(fun () ->
state.Bind(
Subtract 1,
(fun () ->
state.Bind(
GetTotal,
fun total ->
(stat := total; state.Zero())
)
)
)
)
)
)
state.Return "End"
}
while直前のletは、whileと組みになって計算式を形成していると解釈し、let~Whileと、最後のReturnの行をCombineします。すでに悪夢ですが。
let calculatorActions =
state {
do! Add 3
let! total = GetTotal
state.Combine(
let stat = ref total
state.While(
(fun () -> !stat > 0),
state.Delay(
(fun () ->
state.Bind(
Subtract 1,
(fun () ->
state.Bind(
GetTotal,
fun total ->
(stat := total; state.Zero())
)
)
)
)
)
),
state.Delay(
fun () -> state.Return "End"
)
)
}
その前のlet!をBindに展開します。
let calculatorActions =
state {
do! Add 3
state.Bind(
GetTotal,
fun total ->
state.Combine(
let stat = ref total
state.While(
(fun () -> !stat > 0),
state.Delay(
(fun () ->
state.Bind(
Subtract 1,
(fun () ->
state.Bind(
GetTotal,
fun total ->
(stat := total; state.Zero())
)
)
)
)
)
),
state.Delay(
fun () -> state.Return "End"
)
)
)
}
その前のdo!をBindに展開します。
let calculatorActions =
state {
state.Bind(
Add 3,
fun () ->
state.Bind(
GetTotal,
fun total ->
state.Combine(
let stat = ref total
state.While(
(fun () -> !stat > 0),
state.Delay(
(fun () ->
state.Bind(
Subtract 1,
(fun () ->
state.Bind(
GetTotal,
fun total ->
(stat := total; state.Zero())
)
)
)
)
)
),
state.Delay(
fun () -> state.Return "End"
)
)
)
)
}
最後に、不要になったいちばん外側のstate{}を削除すれば、糖衣構文の展開は完了です。
let calculatorActions =
state.Bind(
Add 3,
fun () ->
state.Bind(
GetTotal,
fun total ->
state.Combine(
let stat = ref total
state.Delay(
fun() ->
state.While(
(fun () -> !stat > 0),
state.Delay(
(fun () ->
state.Bind(
Subtract 1,
(fun () ->
state.Bind(
GetTotal,
fun total ->
(stat := total; state.Zero())
)
)
)
)
)
)
),
state.Delay(
fun () -> state.Return "End"
)
)
)
)
何とかここまでたどり着けばコンパイルが通るようになりますので、実際に実行してみて動きが変わっていないことを確認しましょう。
6.まとめ
まとめ、というか「逃げ」なのですが・・・
正直、この式をこの先stateのメソッドの中まで展開していく体力が尽きてしまいました。もし僕にその体力が残っていたとしても、それをそのままここに掲載すると誰もついてこれなくなるような紙面になってしまうと思うので割愛します。
ただ、for展開編までを熟読し、実際に自分の手で展開してもらった人なら、この先どのようになっていくか、stateのメソッドの実装と一緒に追っていくことで理解してもらえると思います。
計算式(ワークフロー)を初めて使ったり、ビルダークラスそのものを書いたりすると、必ずと言っていいほど「訳のわからない型エラー」に悩まされることになります。そういう状況に冷静に対処できるかどうかは、最低限この「糖衣構文を解く」ことができるかできないかにかかっています。もちろん、適当にソースを弄繰り回していて、たまたま型エラーを解消できる、ということもないことはないですが、これを知っておけば無駄な試行錯誤をしなくてすみます。少なくともここまでは自力で展開できるスキルを身に着けておきましょう。
(文責:片山 功士 2012/05/20)
今日: - 人
昨日: - 人
トータル: - 人
最終更新:2012年05月20日 01:52