Arantium Maestum

プログラミング、囲碁、読書の話題

めざそう言語処理系の沼 〜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.DoCont.DoExp.t listを持っている。

ただしこのデータの持つ意味が少し違って、Exp.DoExp.t listDo式の持つすべての部分式なのに対して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も実装完了。

次回

次回は簡単なマクロ機能を実装する。