めざそう言語処理系の沼 〜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も実装完了。
次回
次回は簡単なマクロ機能を実装する。