めざそう言語処理系の沼 〜shift/resetへの旅 〜 その22 モジュール
実装したモナドやmap
、fold_left
などの関数をライブラリ化することで毎回定義し直さなくても済むようにしたい。
その準備段階としてモジュールを実装する。(ファイルから読み込む場合もモジュールとして扱いたいため)
具体的には
(module [...])
という構文でモジュール(第1級オブジェクト)を定義(define ... ...)
でモジュール内での変数束縛M.x
などでモジュールM内の変数xにアクセス
を追加して
(let [M (module [(define x 5) (define y 10)])] (+ M.x M.y))
のような書き方ができるようにする。
今回のコード差分:
Comparing v0.16...v0.17 · zehnpaard/kontlang · GitHub
define
モジュール定義の中でだけ使えるキーワードdefine
を追加する。
まずExp
:
type t = ... | Define of string * t let rec get_free bound free = function ... | Define(_, e) -> get_free bound free e
モジュール内で束縛する変数名を表す文字列と、その変数に束縛する値へと評価される式を持つ。
define
式の自由変数は保持している式の中の自由変数である。
define
式は独自の値とはならず、式の評価も必ずmodule
式の一部として評価される。
なのでExecute.eval
で生のExp.Define
に当たると:
let eval env cont = function ... | Exp.Define _ -> failwith "Define used outside of Module definition"
この通り実行時エラーを吐く。実際にはパーサにも制約をかけてあって、(module [...])
の中以外でdefine
を使うとパースエラーになるのでこの実行時エラーのケースには到達しない(はず)。
また、独自の値にならないのでValcont
モジュールには登場しない。
module
無名モジュールを作成するmodule
式を定義する。
まずExp
:
type t = ... | Module of t list let rec get_free bound free = function ... | Module [] -> free | Module((Define(s,e))::es) -> get_free (s::bound) (get_free bound free e) (Module es) | Module(e::es) -> get_free bound (get_free bound free e) (Module es)
モジュールは式のリストを保持する。define
だけではなく、副作用を起こすような任意の式もモジュール内で記述できるので保持する式にはなんの制限もない。
(module [(define x 10) (define y (+ x 5))]
のように、先にdefine
で定義された変数を後々モジュール内で使えるようにするので自由変数を求める時にmodule
の保持する式を再帰的に探っていきdefine
の場合は後続の式にとってのbound
変数を更新している。
今見直すと再帰するたびにExp.Module
を解体しては新しく作り直していてあまり効率がよくない。fold_left
で綺麗に書けそうなので今度書き直したい。
次にValcont
モジュール:
type valt = ... | Module of (string * valt) list and contt = ... | ModuleExp of Exp.t list * (string * valt) list | ModuleDefine of string * Exp.t list * (string * valt) list
Module
値はモジュール内で束縛されている「変数と値」のリストを保持する。
そして継続としては
- モジュール式の中の
define
式を評価していることを表すModuleDefine
- モジュール式の中の
define
以外の式を評価していることを表すModuleExp
を追加。
ModuleExp
は未評価の式のリストと、これまで評価済みの「変数と値」のリストを持つ。
ModuleDefine
はModuleExp
のデータに加えて「現在評価中のdefine
で束縛される変数の文字列」を持つ。
ではExecute.eval
:
let eval env cont = function ... | Exp.Define _ -> failwith "Define used outside of Module definition"
まずはExp.Define
のケース。これは特殊でdefine
はモジュール定義内でのみ使われることになっていて、Exp.Module
の評価やCont.ModuleDefine
、Cont.ModuleExp
の継続適用の一部として処理されることになる。パーサの仕様上それ以外の状況で出てくることはないはずだが、もし出現してeval
された場合は実行時エラー。
次にExecute.eval
のExp.Module
ケース:
| Exp.Module(Exp.Define(s, e)::es) -> let env' = []::env in let cont' = Cont.add (Cont.ModuleDefine(s, es, [])) @@ Cont.add Cont.Env cont in Eval(env', cont', e) | Exp.Module(e::es) -> let env' = []::env in let cont' = Cont.add (Cont.ModuleExp(es, [])) @@ Cont.add Cont.Env cont in Eval(env', cont', e) | Exp.Module [] -> ApplyCont(env, cont, Val.Module [])
三つのケースが考えられる:
- モジュール内の式の先頭が
define
だった場合 - モジュール内の式の先頭が
define
以外だった場合 - モジュール内で式定義がなかった場合(つまり
(module [])
)
後者はエラーでもいいと思ったのだが、空のモジュールを禁止する強い理由もないので許容。
モジュール内の式の先頭がdefine
だった場合は、変数環境を新しく区切り(モジュール内でdefine
を使って定義された変数がそれ以降のモジュール内定義で使用できるようにするため&モジュール定義が終わったら変数環境から外せるようにするため)、Cont.Env
を継続に追加。さらにCont.ModuleDefine
に「先頭要素以外のモジュール内の式のリスト」と(define x e)
のx
に当たる変数名を保持させて継続に追加。この変数環境と継続で(define x e)
のe
の式を評価する。
モジュール内の式の先頭がdefine
以外だった場合もほぼ同じだが、Cont.ModuleDefine
の代わりにCont.ModuleExp
を使う(変数名は保持しない)。そしてモジュール内の式の先頭要素すべてを評価する((define x e)
のように分解して一部を評価、といった手順は踏まない)。
次にExecute.apply_cont
のCont.ModuleDefine
ケース:
let apply_cont env cont v = match cont with ... | (Cont.ModuleDefine(s, [], svs)::cont')::cont'' -> ApplyCont(env, cont'::cont'', Val.Module((s, v)::svs))
もし未評価の式が残っていなければ、たった今の評価結果である値とそれに対応する変数名を「評価済みの値と変数名のリスト」に追加して、それを保持するVal.Module
値を作成、それを現在の継続に渡す。(Execute.eval
のExp.Module
ケースを見れば最初に行われるのは「現在の変数環境を一つ外す」である)
Cont.ModuleDefine
に未評価の式が残っていて、その先頭の要素がExp.Define
だったケース:
| (Cont.ModuleDefine(s, Exp.Define(s', e')::es, svs)::cont')::cont'' -> let env' = Env.extend_current s v env in let cont''' = Cont.add (Cont.ModuleDefine(s', es, (s,v)::svs)) (cont'::cont'')in Eval(env', cont''', e')
たった今の評価結果である値とそれに対応する変数名を「評価済みの値と変数名のリスト」に追加すると同時に変数環境にも追加。先頭の要素がExp.Define
なので、次の継続もCont.ModuleDefine
となる。その新しいCont.ModuleDefine
を継続に追加してから新しい変数環境と継続でExp.Define
の保持する式を評価する。
Cont.ModuleDefine
に未評価の式が残っていて、その先頭の要素がExp.Define
以外だったケース:
| (Cont.ModuleDefine(s, e::es, svs)::cont')::cont'' -> let env' = Env.extend_current s v env in let cont''' = Cont.add (Cont.ModuleExp(es, (s,v)::svs)) (cont'::cont'')in Eval(env', cont''', e)
たった今の評価結果である値とそれに対応する変数名を「評価済みの値と変数名のリスト」に追加すると同時に変数環境にも追加(これは直前のケースとまったく同じ)。次の継続はCont.ModuleExp
となる。未評価の式の先頭要素を新しい変数環境と継続で評価する。
Execute.apply_cont
でCont.ModuleExp
のケース:
| (Cont.ModuleExp([], svs)::cont')::cont'' -> ApplyCont(env, cont'::cont'', Val.Module svs) | (Cont.ModuleExp(Exp.Define(s', e')::es, svs)::cont')::cont'' -> let cont''' = Cont.add (Cont.ModuleDefine(s', es, svs)) (cont'::cont'')in Eval(env, cont''', e') | (Cont.ModuleExp(e::es, svs)::cont')::cont'' -> let cont''' = Cont.add (Cont.ModuleExp(es, svs)) (cont'::cont'')in Eval(env, cont''', e)
ModuleDefine
から少々修正しただけなので詳細は割愛。
ここまでで(module [(define x 5) (define y 10) (define f (fn [a b] (+ a b)))])
のようなモジュールが作成できるようになった。
モジュール変数にアクセス
モジュールを作ったはいいが、中の変数にアクセスする方法が必要。
というわけでM.x
やM.N.y
のようにドット記法でモジュールの中身にアクセスできるようにする。M.N.y
は「M
モジュール内の変数N
に束縛されているモジュールのy
変数にアクセス」という意味になる。
まずはExp
モジュールで式を定義:
type t = ... | MVar of string list let rec get_free bound free = function ... | MVar [] -> free | MVar(s::_) -> if (List.mem s bound) then free else s::free
MVar
はモジュール変数の意。M.N.y
だとMVar ["M"; "N"; "y"]
と表現されることになる。
自由変数検出に関しては、先頭の要素のみ検出される。つまりM.N.y
というモジュール変数が出てきた場合、M
はもしかすると自由変数かもしれないが、N
とy
は自由変数にはなり得ない。
珍しくlexer
のコードも載せてみる:
let char = ['a'-'z' 'A'-'Z' '+' '-' '*' '/' '<' '>' '=' '!' '?' '_'] let number = ('0'|['1'-'9'] digit*) let variable = char (char|digit)* let mvariable = variable '.' variable ('.' variable)* rule f = parse ... | mvariable as s { Parser.MVAR (String.split_on_char '.' s) }
mvariable
という正規表現っぽいパターンにマッチした文字列をString.split_on_chat '.'
で分割したリストをMVAR
としてパーサに渡している。
Execute.eval
:
let eval env cont = function ... | Exp.MVar([]) -> failwith "Evaluating invalid/empty module var" | Exp.MVar(s::ss) -> let m = Env.find s env in let f m s = match m with | Val.Module svs -> (match List.assoc_opt s svs with | Some n -> n | None -> failwith @@ Printf.sprintf "Member %s of module does not exist" s) | _ -> failwith @@ "Accessing member of non-module" in let v = List.fold_left f m ss in ApplyCont(env, cont, v)
Exp.MVar
が空リストということはパーサ上あり得ないが、パターンマッチの網羅性のために実行時エラーになるケースを追加しておく。
Exp.MVar
の変数名リストに要素が含まれている場合はまず先頭要素の変数を現在の変数環境の中から探して、その値を起点にfold_left
で残りの変数名リストを解決していく。変数が見つからない場合と最後の変数名以外がモジュールでない値であった場合には実行時エラーとなる。
これでモジュールの基本機能である「作成」と「アクセス」は完了。
再掲になるが:
(let [M (module [(define x 5) (define y 10)])] (+ M.x M.y))
のような書き方ができて、結果は15になる。
次回
次は外部ファイルからモジュールを読み込めるようにする。