Add/Subtract/Multiply/Divide展開編
これらの関数は四つありますが、見て分かるように中の演算子が違うだけで内容的にはほぼ同じものです。なので、代表としてAddを展開し、その結果を見て他の三つも展開することにしましょう。
関数展開の基本は、内側から、前からの順番に行います。ただ、どの順でやってもできないということはなく、途中の分かりやすさが若干違うだけのことも多いので、あまり展開順にこだわらず、どこからやっても展開できるような基礎体力をつけておくことも重要です。
というわけで、まな板の上にAdd関数の定義を載せます。VisualStudioを使っている人は、StateBuilderやGetState、SetState、StatefulFunc、Runなどの定義と一緒にソース上に貼り付け、コンパイルエラーが出ない状態にしてください。。
let Add x =
state {
let! currentTotal, history = GetState
do! SetState (currentTotal + x, (sprintf "%dを加算" x) :: history)
}
1.まず、GetStateとSetStateが関数であるためその定義と置き換える。
二つの関数の定義は以下のとおり。
let GetState = StatefulFunc (fun state -> state, state)
let SetState newState = StatefulFunc(fun prevState -> () , newState)
GetStateは引数がないためそのまま置き換えるだけ。
SetStateは、引数に与えられている「(currentTotal + x, (sprintf "%dを加算" x) :: history)」を、関数定義の右辺のnewStateと置き換えます。
置き換えたのが以下のような状態です。
let Add x =
state {
let! currentTotal, history = StatefulFunc(fun state -> state, state)
do! StatefulFunc (fun prevState -> () , (currentTotal + x, (sprintf "%dを加算" x) :: history))
}
これで、Addしか使わないならもうGetStateやSetStateの定義は不要になりますが、置き換えたAddは、これまでと同じように動くはずです。Alt+Enterで試して、これまでと実行結果が変わってないことを確認してください。
今後このような置き換えによる式の展開、変形を延々と繰り返していくことになりますが、一つ作業をしてコンパイラがエラーを出さなくなったら、その都度Alt+Enterで動作が変わってないことを確認してください。
2.return()の補完
プログラミングF#のどこにも書いていないのですが、計算式がdoのような値を返さない式で終わった場合は、コンパイラによってreturn ()が補完されてから解釈されます。僕はこれに気づくまで数日苦しみましたが、分かってしまえば当たり前のことといえます。
let Add x =
state {
let! currentTotal, history = StatefulFunc(fun state -> state, state)
do! StatefulFunc (fun prevState -> () , (currentTotal + x, (sprintf "%dを加算" x) :: history))
return ()
}
3.returnの置き換え(ワークフロー展開1)
いよいよワークフローの糖衣構文を解いていきます。
この作業は、途中コンパイルが通らず、うまくできているのか分からない状態になりますが、大気圏に突入するつもりで頑張ってください。
最初は簡単なreturnの展開から始めます。
困ったことにプログラミングF#には載ってないのですが、コンパイラがワークフローの糖衣構文を展開する際の置き換えルールがあります。幸い「実践F#関数型プログラミング入門」には載っているので、ここから引用します。
return exp ⇒ b.Return(exp)
expはreturnの引数に与えられた何らかの式です。ここでは()になります。
bはワークフロービルダのオブジェクトで、ここではstateがそれにあたります。
これだけあれば最初の展開は可能です。
let Add x =
state {
let! currentTotal, history = StatefulFunc(fun state -> state, state)
do! StatefulFunc (fun prevState -> () , (currentTotal + x, (sprintf "%dを加算" x) :: history))
state.Return ()
}
ここからしばらくは前半最初のヤマ場となります。VisualStudio上で作業をしている場合はコンパイルエラーを表す赤い波線が出たままになっていると思いますが、次のステップが終わるまで我慢して下さい。
4.do!の置き換え(ワークフロー展開2)
return同様、do!の糖衣構文の展開ルールも「実践F#関数型プログラミング入門」から引用します。
do! expr in expr2 ⇒ b.Bind(expr, fun() -> expr2)
なにやら怪しげなルールですが、これ(とlet!の展開ルール)こそがワークフローを形成している最も重要な変換ルールですので毎日暗唱するぐらいのつもりでいてください。ちなみに上のdo!についているinはソース上では省略されています。
まず、二行目のdo!から展開します。なぜ2行目から展開するのかと言いますと、展開すると2行目のdo!のほうが「内側」になるからです。やってみれば分かります。
上記の変換ルールでexprにあたるのは、do!の右にある式全部です。expr2にあたるのは次の行以降、stateの閉じ括弧までの式全部になります。この、「閉じ括弧までの式全部」を、「残りの計算」と言う意味で「継続」と呼びます。末尾再帰で出てきた「継続」と内部的には同じものですが捉え方はかなり違うので注意してください。
2行目のdo!から見ると継続は3行目だけになりますが、1行目のlet!から見ると2行目と3行目の両方を結合した処理が継続となります。
それでは、2行目のdo!を展開してみましょう。
let Add x =
state {
let! currentTotal, history = StatefulFunc(fun state -> state, state)
state.Bind(
StatefulFunc(fun prevState -> () , (currentTotal + x, (sprintf "%dを加算" x) :: history)),
(fun () -> state.Return ())
)
}
- do! → state.Bind(
- do!の右にあった式 → state.Bindの最初の引数
- do!の次行以降の式 → state.Bindのに番目の引数
になっていることを確認してください。
この変換は、F#のコンパイラが行っていることで、定義したり変更したりすることはできません。
5.let!の置き換え(ワークフロー展開3)
さらに最初のlet!を置き換えます。let!の展開ルールは以下のようなものです。
let! pat = expr in expr2 ⇒ b.Bind(expr, fun pat -> expr2)
ちなみに上のlet!についているinもソース上では省略されています。
これをよく見ながら、半ば機械的にソースを書き換えていきます。
let Add x =
state {
state.Bind(
StatefulFunc(fun state -> state, state),
(fun (currentTotal, history) ->
state.Bind(
StatefulFunc(fun prevState -> () , (currentTotal + x, (sprintf "%dを加算" x) :: history)),
(fun () -> state.Return ())
)
)
)
}
patとなっているcurrentTotal, historyと、二つ目の引数の前後に括弧を補いました。
最後に、不要となった一番外側のstate{}を削除すると、コンパイルが通るようになっているはずです。
let Add x =
state.Bind(
StatefulFunc(fun state -> state, state),
(fun (currentTotal, history) ->
state.Bind(
StatefulFunc (fun prevState -> () , (currentTotal + x, (sprintf "%dを加算" x) :: history)),
(fun () -> state.Return ())
)
)
)
ここで一旦Alt+Enterで実行してみて、プログラムの動きが変わっていないことを確認してください。
元のプログラムの面影はほとんど無くなってしまいましたが、これは元の式と同じ式なのです。
6.Returnを定義に置換する(ワークフロー定義展開1)
まだまだ続きます。
これからの作業は、state.Returnやstate.Bindの記述を、StatefulFuncのmemberとして定義された関数と置き換えていく作業です。
Returnメンバ関数は以下のように定義されています。
member this.Return(x : 'a) =
StatefulFunc(fun initialState -> x, initialState)
式の中でReturnには()が引数になっていますので、この定義のxを()に置き換えて、中ほどのstate.Returnを以下のようにします。
let Add x =
state.Bind(
StatefulFunc(fun state -> state, state),
(fun (currentTotal, history) ->
state.Bind(
StatefulFunc(fun prevState -> () , (currentTotal + x, (sprintf "%dを加算" x) :: history)),
(fun () -> StatefulFunc(fun initialState -> (), initialState))
)
)
)
7.内側のBindを定義に置換する(ワークフロー定義展開2)
続いて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
)
引数は二つ。resultとrestOfComputationですが、今Bindに与えられている二つの式を、= 以降の式のresult、restOfComputationと置き換えてしまうのです。
ただし注意があります。Bindの定義にある
let result, updatedState = Run result initialState
の、=の左にあるresultは、引数にわたってきたresultとは型も値も全くの別物で、Runの実行結果に同じ名前を付けているだけです。次の行のresultもそうです。置き換えるのは、この行の=の右にあるresultだけですので間違えないで下さい。このあたりこのサンプルは初学者に配慮が足りないなぁと思ってしまいます。
ぼやいていても仕方がないので、元々do!だった内側のBindからやってみましょう。
let Add x =
state.Bind(
StatefulFunc (fun state -> state, state),
(fun (currentTotal, history) ->
StatefulFunc(fun initialState ->
let result, updatedState =
Run (StatefulFunc(fun prevState -> () , (currentTotal + x, (sprintf "%dを加算" x) :: history))) initialState
Run ((fun () -> StatefulFunc(fun initialState -> (), initialState)) result) updatedState
)
)
)
BindのあったところがStatefulFuncになり、二つの引数がresultとrestOfComputationと置き換わっているのが分かるでしょうか。
resultの置き換えには括弧を補っています。
8.外側のBindを定義に置換する(ワークフロー定義展開3)
ちょっと大変なことになってきた感じがしなくもないですが、機械の体を手に入れたつもりで外側のBindも展開します。
restOfComputationにあたるものが、5行にわたる式であることに気をつけてください。
let Add x =
StatefulFunc(fun initialState ->
let result, updatedState = Run (StatefulFunc(fun state -> state, state)) initialState
Run (
(fun (currentTotal, history) ->
StatefulFunc(fun initialState ->
let result, updatedState =
Run (StatefulFunc(fun prevState -> () , (currentTotal + x, (sprintf "%dを加算" x) :: history))) initialState
Run ((fun () -> StatefulFunc(fun initialState -> (), initialState)) result) updatedState
)
) result) updatedState
)
これで、ワークフローの展開ができました。すでにStateBuilderクラスもそのオブジェクトstateもこの式からは無くなっているので、この時点でこれらのクラスがなくてもコンパイルが通るようになっているはずです。
このように、ワークフローの展開は以下のような2段階の構成になっています。
- do!やlet!、returnを、state.Bindやstate.Returnを使った式に置き換える。(糖衣構文の展開)
- state.Bindやstate.Returnを、StateBuilderの定義に従って置き換える。
この作業を経ることで、ワークフローがどのような式を作成するかを知ることができます。
知ることができます、と言われても、上でできている式はかなりのものです。パッと見て意図が分かればたいしたものですが、僕は分からないので、これからは上の式を簡約していきます。ここから式は小さくなっていく一方ですのでご安心を。
9.RunでStatefulFuncの皮をむく
まず、内側のStatefulFuncの1行目にあるRunを簡約してみます。Runの定義は以下のようなものです。
let Run (StatefulFunc f) initialState = f initialState
これを見ると、関数fという中身をもつStatefulFuncとinitialStateの二つを引数にとり、initialStateに関数fを適用した結果を返す機能を持っていることがわかります。つまり、
Run (StatefulFunc(fun a -> a * a) c
という式があったら、StatefulFuncから関数(fun a -> a * a)を取り出し、その引数aにcを代入して「c*c」としていいということです。
Run (StatefulFunc(fun a -> a * a) c <=> c * c
この二つの式は等価なのです。
これを念頭に置くと、、
Run (StatefulFunc (fun prevState -> () , (currentTotal + x, (sprintf "%dを加算" x) :: history))) initialState
は、
() , (currentTotal + x, (sprintf "%dを加算" x) :: history)
と同じであることがわかります。
ちょっと引っかかるのが、Runの二番目の引数になっているinitialStateが消滅してしまっていることです。
これは、StatefulFuncの中にある関数の仮引数prevStateが関数の中で使われていないためなのですが、どこか釈然としませんね。
こういうちょっとバカにされているような気分になることが、StatefulFuncの簡約にはたびたび出てきます。あまり気にしないようにしましょう。
と言うわけで、式全体は以下のようになります。
let Add x =
StatefulFunc(fun initialState ->
let result, updatedState = Run (StatefulFunc(fun state -> state, state)) initialState
Run (
(fun (currentTotal, history) ->
StatefulFunc(fun initialState ->
let result, updatedState = () , (currentTotal + x, (sprintf "%dを加算" x) :: history)
Run ((fun () -> StatefulFunc(fun initialState -> (), initialState)) result) updatedState
)
) result) updatedState
)
式の真ん中へんで右に尖がっていたところが落ち着きましたね。
ところで、その行を改めてみていると、let束縛の左右にタプルが来ていて、以下のように束縛されていることが分かります。
let result = ()
let updatedState = (currentTotal + x, (sprintf "%dを加算" x) :: history)
これを、次の式のresult、updateStateと置き換えてしまうと、この行は不要になります。
let Add x =
StatefulFunc(fun initialState ->
let result, updatedState = Run (StatefulFunc(fun state -> state, state)) initialState
Run (
(fun (currentTotal, history) ->
StatefulFunc(fun initialState ->
Run ((fun () -> StatefulFunc(fun initialState ->
(), initialState)) ()) (currentTotal + x, (sprintf "%dを加算" x) :: history)
)
) result) updatedState
)
1行減りましたね。
10.fun () に()を与える。
今しがた変形して1行にまとめた行を見ると、Runの最初の引数になっている式が、以下のような形をしていることに気がつきます。
(fun () -> StatefulFunc(fun initialState -> (), initialState)) ()
これは、unitを引数にとる関数にunitを与えているわけですから、直ちに関数から取り出すことができます。
StatefulFunc(fun initialState -> (), initialState)
このように、unitを引数にとる関数にunitを渡す行為は一見無駄なように思えますが、関数型言語の世界では「遅延評価」というものを形作る重要な考え方になります。頭の隅に置いといてください。
さて、行全体としては
Run (StatefulFunc(fun initialState -> (), initialState)) (currentTotal + x, (sprintf "%dを加算" x) :: history)
のようになりました。
この式は、先ほどのRunの展開と同じように変形できますので、
(), (currentTotal + x, (sprintf "%dを加算" x) :: history)
と簡約できます。
ずいぶん短くなってしまったので、前後の括弧を取り込んで行をまとめて見ると、全体として以下のようになります。
let Add x =
StatefulFunc(fun initialState ->
let result, updatedState = Run (StatefulFunc(fun state -> state, state)) initialState
Run (
(fun (currentTotal, history) ->
StatefulFunc(fun initialState -> (), (currentTotal + x, (sprintf "%dを加算" x) :: history))
) result) updatedState
)
11.initialStateの正体
少し目先を変えて、外側のStatefulFuncの1行目に出てくるRunを先に簡約することにしましょう。
let result, updatedState = Run (StatefulFunc(fun state -> state, state)) initialState
まずRunでStatefulFuncの中身を取り出して、initialStateに適用します。するとこんな式になります。
let result, updatedState = initialState, initialState
というわけで、result, updatedStateはともに同じ値initialStateになりました。これを下のresult, updatedStateと置き換えると以下のようになります。
let Add x =
StatefulFunc(fun initialState ->
Run (
(fun (currentTotal, history) ->
StatefulFunc(fun initialState -> (), (currentTotal + x, (sprintf "%dを加算" x) :: history))
) initialState) initialState
)
ここでRunに続く関数に注目してください。引数は(currentTotal, history)というタプルになっています。ところがその関数が適用しようとしている値はinitialStateという一つの束縛変数です。これはどうしたことでしょう。
実は簡単なことなのです。initialStateはひとつの変数ですがその型は何かのタプルなのです。どうせ(currentTotal, history)に適用されるのですから、initialStateは(currentTotal, history)という名前の2変数のタプルとしてしまいましょう。
let Add x =
StatefulFunc(fun (currentTotal, history) ->
Run (
(fun (currentTotal, history) ->
StatefulFunc(fun initialState -> (), (currentTotal + x, (sprintf "%dを加算" x) :: history))
) (currentTotal, history) ) (currentTotal, history)
)
こうすることで、2行目の関数の引数が(currentTotal, history) となり、その内側の関数が適用する変数と同じになるので、内側の関数に(currentTotal, history) を適用する際に名前を変える必要がなくなります。
ちょっと変な感じですが、この簡約に問題ないことが納得できるまでよく考えて下さい。
let Add x =
StatefulFunc(fun (currentTotal, history) ->
Run (
StatefulFunc(fun initialState -> (), (currentTotal + x, (sprintf "%dを加算" x) :: history))
) (currentTotal, history)
)
12.あと少し
最後にRunをはずして中身をまた(currentTotal, history) に適用します。
let Add x =
StatefulFunc(fun (currentTotal, history) ->
(fun initialState -> (), (currentTotal + x, (sprintf "%dを加算" x) :: history)) (currentTotal, history)
)
この関数もinitialStateが中の式で使われていないため引数は捨てられます。なんだかなぁ。
let Add x = StatefulFunc(fun (currentTotal, history) -> (), (currentTotal + x, (sprintf "%dを加算" x) :: history))
というわけで、たったこれだけの式になってしまいました。なんだかバカにされている気がしてきませんか?
この式をじっと見ると、なるほどRunをかませて(0, [])を与えると()と、加算値と文字列のリストのタプルが返るだろうということが読み取れます。これがAddの正体だったのです。
同様にSubtract、Multiply、Divideも定義できます。
let Subtract x = StatefulFunc(fun (currentTotal, history) -> (), (currentTotal - x, (sprintf "%dを減算" x) :: history))
let Multiply x = StatefulFunc(fun (currentTotal, history) -> (), (currentTotal * x, (sprintf "%dを積算" x) :: history))
let Divide x = StatefulFunc(fun (currentTotal, history) -> (), (currentTotal / x, (sprintf "%dを除算" x) :: history))
(文責:片山 功士 2011/12/29)
今日: - 人
昨日: - 人
トータル: - 人
最終更新:2012年02月03日 01:57