めざそう言語処理系の沼 〜shift/resetへの旅 〜 その12 文字列、入出力とDo
今まで作ってきた言語には副作用がなかったが、やはり入出力くらいはほしいよね、というわけで
- 式と値に文字列を追加
- 標準入出力からの読み取り・書き込みのための組み込み関数
- 副作用のある処理を順次実行して、最後の式の値をとる
Do
式
を言語に追加する。
前回からの差分:
Comparing v0.11...v0.12 · zehnpaard/kontlang · GitHub
式と値に文字列を追加
式のExp.t
に追加:
type t = ... | Str of string
値のVal.t
にも追加:
type t = ... | Str of string
式を評価して値にするExecute.eval
:
let eval env cont = function ... | Exp.Str s -> ApplyCont(env, cont, Val.Str s)
これだけ。
標準入出力
組み込み関数にいくつか追加していく。
まず複数の文字列を結合するconcat
:
let concat_op ss = let f = function | Val.Str s -> s | _ -> failwith "Non-string passed to concat" in Val.Str (String.concat "" @@ List.map f ss)
文字列を標準出力するprint
と出力の最後に改行を入れるprintln
:
let print_op = function | [Val.Str s] -> print_string s; Val.Nil | _ -> failwith "Non-string passed to print" let println_op = function | [Val.Str s] -> print_endline s; Val.Nil | _ -> failwith "Non-string passed to println"
これらは文字列を引数にとり、Val.Nil
を返す。
標準入力から文字列を取ってくるread
:
let read_op = function | [] -> Val.Str (read_line ()) | _ -> failwith "Args passed to read"
これらはすべてOCamlの関数をラップしているだけなので楽。
あとは変数環境に入るようbuiltins
に名前と一緒に入れるだけ:
let builtins = [ ... ; "concat", Val.Op("concat", concat_op) ; "print", Val.Op("print", print_op) ; "println", Val.Op("println", println_op) ; "read", Val.Op("read", read_op) ]
Do
式
副作用のある処理を記述できるようになったので、そういった処理を順次行うための構文がほしくなる。
というわけでこんなDo
式を導入する:
(do [(println "hello") (print "please enter your name: ") ( read)])
最後の式の値がDo
式全体の値となるので、上記のコードの場合、read
で受け取った入力の文字列が結果となる。
Exp.t
に追加:
type t = ... | Do of t list
Cont
にも追加:
type cont = ... | Do of Exp.t list
Exp.Do
もCont.Do
もExp.t list
を持っている。
ただしこのデータの持つ意味が少し違って、Exp.Do
のExp.t list
はDo
式の持つすべての部分式なのに対してCont.Do
の場合は「まだ未評価の式」を保持する形となる。
Execute.eval
:
let eval env cont = function ... | Exp.Do(e::es) -> Eval(env, Cont.Do es::cont, e) | Exp.Do([]) -> failwith "Evaluating empty do"
Exp.Do
を評価するのには、Exp.t list
のtailを持つCont.Do
を継続に追加して、Exp.t list
のheadを評価する。
Execute.apply_cont
:
let apply_cont env cont v = match cont with ... | Cont.Do(e::es)::cont' -> Eval(env, Cont.Do es::cont', e) | Cont.Do([])::cont' -> ApplyCont(env, cont', v)
値をCont.Do
が先頭にくる継続に適用するのに、Cont.Do
の持つExp.t list
が空リストかどうかで場合分けする。
空リストではない場合、apply_cont
の引数である値は無視して、Exp.t list
のtailを持つCont.Do
を継続に追加して、Exp.t list
のheadを評価する。
空リストの場合、apply_cont
の引数である値をそのまま残りの継続に適用する。
これでDo
も実装完了。
次回
次回は簡単なマクロ機能を実装する。