めざそう言語処理系の沼 〜shift/resetへの旅 〜 その19 shift/resetでつくるStateモナド
shift
とreset
を使ってStateモナドを実装する。ちなみに今まで実装したモナドの中で随一のややこしさである。
Stateモナドはその名のとおり、計算に局所的な「状態」を持たせることができるモナドである。状態とは「値を参照したり書き換えたりできる何か」を指す。
reify
、get
、put
、run_state
今回reify
が複雑になっている:
(let [reify (macro [expr] (reset (let [result expr] (fn [state] (cons result state)))))] ...)
最終的にreify
が返すのは「初期状態」を受けとり「expr
の評価結果と最終状態のタプル」を返す関数となる。例によってreify
はマクロなのでexpr
はreset
の中でresult
に束縛される時点までは評価されない。
Stateモナドにはreflect
に直接当たる処理はなく、get
とput
の二つの関数がモナディックな処理を担う。
まず現在のStateの値をとってくる関数であるget
:
(let [get (fn [] (shift [k] (fn [state] ((k state) state))))] ...)
そしてStateの値を更新した上で評価結果はnil
になるput
:
(let [put (fn [val] (shift [k] (fn [state] ((k nil) val))))] ...)
これら二つの関数は形がけっこう似ている。どちらもshift
を使って限定継続をとり、reset
内の式を(fn [state] ((k X) Y))
という形の関数に置き換えてしまう。
X
は「この関数呼び出しの評価結果」で、Y
は「この関数呼び出し以降のstateの値」である。
get
の場合はX
は現在のstate、Y
も(状態を変更しないので)現在のstateで(fn [state] ((k state) state))
となる。
put
の場合はX
はnil
、Y
はput
の引数であるval
で、(fn [state] ((k nil) val))
となって元々のstateは破棄されて新しい値に更新される。
reify
の中でget
やput
が呼ばれるかどうかに関わらず、reify
の結果は(fn [state] ...)
という関数であることがわかる。
その関数に対して初期状態値を適用してやるrun_state
関数を定義する:
(let [run_state (fn [m state] (m state)))] ...)
Stateモナドな値と初期状態値をとって関数適用するだけ。
これらの関数があればStateモナドが使える。ここからはいくつか使用例を紹介する。
使用例1
get
もput
も使わず、reify
に直接値を渡す場合:
(run_state (reify 5) 0)
reify
の部分は、内部でshift
が呼ばれなかったのでそのまま(fn [state] (cons 5 state))
という関数になり、run_state
で0をstate引数として渡されて、最終的な結果は(5 . 0)
。
使用例2
get
を使ってみる:
(run_state (reify (get)) 0)
今回はreify
の中にget
があるのでshift
が実行され、
(reify (get))
は
(fn [state] ((k state) state))
に置き換えられる。
この時点での限定継続k
を関数として表現すると(fn [v] (let [result v] (fn [state] (cons result state))))
となるので、(reify (get))
はこの:
(fn [state] ((k state) state))
からk
を置き換えて:
(fn [state] ((let [result state] (fn [state'] (cons result state'))) state))
簡約して:
(fn [state] ((fn [state'] (cons state state')) state))
さらに簡約して
(fn [state] (cons state state))
となり、(run_state (reify (get)) 0)
の結果は(0 . 0)
となる。
使用例3
put
を使ってみる:
(run_state (reify (put 5)) 0)
これも(reify (put 5))
が
(fn [state] ((k nil) 5))
に置き換えられ、限定継続k
はやはり(fn [v] (let [result v] (fn [state] (cons result state))))
となるので
(fn [state] (((fn [v] (let [result v] (fn [state] (cons result state)))) nil) 5)) (fn [state] ((let [result nil] (fn [state] (cons result state))) 5)) (fn [state] ((fn [state] (cons nil state)) 5)) (fn [state] (cons nil 5))
となり、(run_state (reify (put 5)) 0)
の結果は(nil . 5)
になる。
使用例4
最後に「shift/resetプログラミング入門」の例題を実装してみる:
(run_state (reify (let* [(tick (fn [] (put (+ (get) 1)))) (_ (tick)) (_ (tick)) (a (get)) (_ (tick))] (- (get) a))) 0)
- 「状態をインクリメントする」
tick
関数をget
とput
を使って定義 tick
を複数回使って状態を更新- 途中の状態の値を変数
a
に束縛 - 最終的な状態の値と
a
との差を求める
という処理になっており、最終的な結果は(1 . 3)
(求めた差と最終状態のタプル)となる。
このコードの全容:
(let [(reify (macro [expr] (reset (let [result expr] (fn [state] (cons result state)))))) (get (fn [] (shift [k] (fn [state] ((k state) state))))) (put (fn [val] (shift [k] (fn [state] ((k nil) val))))) (run_state (fn [m state] (m state)))] (run_state (reify (let* [(tick (fn [] (put (+ (get) 1)))) (_ (tick)) (_ (tick)) (a (get)) (_ (tick))] (- (get) a))) 0))
まとめ
shift
とreset
を使ってHaskellのStateモナドと同等のことができるようになった。他にも関数適用する形で状態を更新するmodify
などがあるが、tick
と同じようにget
とput
の組み合わせで書ける。
do
記法のように特殊なモナド構文を使わず、reify
の中では普通のlet
、let*
や関数呼び出しなどを使って処理が記述できるところは面白い。
次回
別ファイルに記述されたスタンダード・ライブラリを読み込む機能がほしい。その前準備として簡単なモジュール機構を実装する。