めざそう言語処理系の沼 〜shift/resetへの旅 〜 その16 shift/reset
ついにshift
/ reset
を実装するところまで到達!
reset
で区切った限定継続をshift
で取り出して使う、という言語機能だ。
shift/resetプログラミング入門に出てくるような処理が実装できるようになる:
(* (reset (+ 3 (shift [k] (* 5 (k 2))) -1)) 2)
この例だと限定継続k
が(fn [x] (+ 3 x -1))
となって、(reset ...)
部分が(* 5 (+ 3 2 -1))
に入れ替えられ、全体の式は(* (* 5 (+ 3 2 -1)) 2)
で結果は40となる。
前回との差分:
Comparing v0.15...v0.16 · zehnpaard/kontlang · GitHub
コード解説
まずはExp.t
にReset
とShift
式を追加する:
type t = ... | Reset of t | Shift of string * t
次に値を追加。shift
やreset
ではなく、それらを使った結果として取り出せる「限定継続」を値として表す。なのでValcont
モジュールのvalt
型にCont
を追加:
type valt = ... | Cont of string * (string * valt) list list * contt list
保持する要素としては
- 仮引数を表す文字列(限定継続の場合は必ず引数1になる)
reset
からshift
の間で追加された変数環境reset
からshift
の間の継続
の三点。特に変数環境については、変数名と値のリストではなく、変数名と値のリストのリストであることに注意。
例えばこんなプログラムがある場合:
(reset (let [(x 10) (y 11) (z 12)] (+ x (* y z) (let [(a 1) (b 2) (c 3)] (+ a b c (shift [k] k))))))
(let [(x 10) (y 11) (z 12)]
と(let [(a 1) (b 2) (c 3)]
、そしてreset
自体のクロージャの三つの「変数名と値のリスト」を保持する必要がある。(限定継続は保存されてプログラムのまったく違うところで呼び出される可能性もあるため、関数と同じくクロージャを保持する必要もある)
Valcont
のCont
モジュールの方にはpop
関数を追加:
let pop = function | [] -> failwith "Popping empty continuation" | cont::cont' -> cont, cont'
「shift
から直近のreset
までの限定継続」というのはcontt list list
である継続の先頭のリストで表されている。なのでpop
はその先頭のリストとその残りをわけて返す関数。
式を追加したのでMacro
モジュールにも場合分けのケースを追加:
let substitute e ss es = let env = List.combine ss es in let rec f = function ... | Exp.Reset e -> Exp.Reset (f e) | Exp.Shift(s, e) -> Exp.Shift(s, f e) in f e
マクロの中にshift
やreset
が出てきてもその中の式をマクロ置換できるようにしている。
Utils
モジュールにヘルパ関数を二つ追加:
let break_off n xs = let rec f n acc xs = if n = 0 then List.rev acc, xs else match xs with | [] -> failwith "Not enough elements in list to break off" | x::xs' -> f (n-1) (x::acc) xs' in f n [] xs let rec count y = function | [] -> 0 | x::xs -> (if x = y then 1 else 0) + count y xs
あるリストを、先頭n
個の要素を取る形で二つのリストに分割する関数break_off
と、ある値と一致する要素の数を数えるcount
。ここらへんはPervasivesのList
モジュールに入っていても良さそうだが・・・ CoreやBatteriesには入っていそう。
Env
モジュール:
let pop = function | [] -> failwith "Popping empty environment" | svs::env -> svs, env let rest = function | [] -> failwith "Taking rest of empty environment" | _::env' -> env'
今までEnv.pop
と呼んでいた関数をEnv.rest
と名付け直した。これは先頭の要素を捨てて、残りのリストを返すもの。Env.pop
は先頭の要素と残りのリスト両方を返す関数として再定義。
最後にこれらを踏まえてExecute
モジュールに変更を加える。
まずExecute.eval
:
let eval env cont = function ... | Exp.Reset e -> let free = Utils.dedupe @@ Exp.get_free [] [] e in let fvals = List.map (fun v -> v, Env.find v env) free in Eval(Env.extend_list fvals env, []::cont, e)
reset
式の中に入った時点でクロージャを作って変数環境と継続に追加している。shift
で捕捉できるようにするためで、reset
内でshift
に遭遇しなければこのクロージャはあってもなくても同じ。
Execute.eval
のExp.Shift
ケース:
... | Exp.Shift(s, e) -> let free = Utils.dedupe @@ Exp.get_free [s] [] e in let fvals = List.map (fun v -> v, Env.find v env) free in let cont', cont'' = Cont.pop cont in let n = 1 + Utils.count Cont.Env cont' in let env', env'' = Utils.break_off n env in let contv = Val.Cont(s, env', cont') in let closure = (s, contv)::fvals in Eval(Env.extend_list closure env'', []::cont'', e)
限定継続とその継続に含まれるCont.Env
に対応するだけの変数環境をVal.Cont
に保持させ、shift
に含まれる式のクロージャを作成しVal.Cont
も変数環境に追加して、最終的にshift
に含まれる式を(限定継続の外の継続で)評価する。余談だが、ここのreset
とshift
どちらもクロージャを持つという点がなかなかわからなくて実装中はバグの元だった。
次にExecute.apply
でVal.Cont
が呼び出された時の処理:
let apply_cont env cont v = match cont with ... | (Cont.Call([], vs) :: cont')::cont'' -> (match List.rev (v::vs) with ... | Val.Cont(s, svss, cont''') :: vs' -> let argcount = List.length vs' in if argcount = 1 then let final_cont = cont'''::cont'::cont'' in let f env svs = Env.extend_list svs env in let env' = List.fold_left f env (List.rev svss) in ApplyCont(env', final_cont, v) else failwith @@ Printf.sprintf "Continuation %s called with incorrect number of args: expected 1 received %d" s argcount
- 実引数が1であることを確認し
Val.Cont
に保持されていた限定継続を現在の継続の先頭に追加Val.Cont
に保持されていた(1つ以上の)変数環境を、現在の変数環境の先頭に追加- 新しい継続に実引数の値を渡す
という流れになっている。実引数が2個以上なら実行時エラー。
最後に、reset
がない状態でshift
が呼ばれたらどうなるのか?という問題に対処する。対応するreset
がない場合、shift
は現在の限定すべてをとってくることになっている。そうすると問題になるのがそのとってきた限定(されてない)継続のクロージャで、reset
で作成していないのでうまく捕捉できない。
一番簡単な解決方法として、プログラム全体をreset
で括ってしまうという手段にでた。
let run e = trampoline @@ Eval(Builtins.load Env.empty, Cont.final, Exp.Reset e) ... let run' e = trampoline' @@ Eval(Builtins.load Env.empty, Cont.final, Exp.Reset e)
パースしたASTをトランポリンで評価していく前にExp.Reset
で包んでしまう。これだけでプログラム全体を評価し始めると一番最初に全体の自由変数が捕捉されてクロージャに束縛される。対応するreset
のないshift
で作成される限定継続はこのクロージャを保持することになる。
これで限定継続機能のshift
とreset
の実装完了!
次回はこれらを使っていろいろと他の言語機能(可変な状態やバックトラックなど)らしきものを作ってみる。