let x = 1 in let f = fun y -> x + y in let x = 2 in f (x + 3)OCaml は静的束縛の言語であるので、 このプログラムの実行における let f = ... という部分で、 f に束縛される値を (fun y -> x + y) という式、と 考えることはできない。 なぜなら、そう考えると、上記の式は、
let x = 1 in let x = 2 in (fun y -> x + y) (x + 3)という式と同じになってしまうが、これは、OCaml で実行すると 7 になる。
正しく実行するためには、 「(fun y -> x + y) 」における x の値が 1 である、という情報を持ってお く必要がある。なるほど! 関数そのものを(引数を与えずに)実行して、(関数としての)値を得るときには、 その時点での環境を保存しておく必要がある。
この考えに基づき、値の型の定義を以下のように改善する。
(* 値の型 *) type value = | IntVal of int | BoolVal of bool | FunVal of string * exp * ((string * value) list)前回に比べて、FunVal の引数の型が少し複雑になった。 (string * value) list というのは、環境の型であったので、要するに、 (fun x -> 1) という式を計算した結果としての値は、 FunVal ("x", IntLit 1) ではなく、 FunVal ("x", IntLit 1, env) となる。ここで env というのは、(fun x -> 1) を評価しているときの環境である。
なお、関数型プログラム言語の処理系においては、上記のような 「関数を評価した結果」であるところの「値としての関数(関数の引数、本体 に、環境を追加したもの)」を、「関数クロージャ (function closure、ある いは、単に closure)」と呼ぶ。
値を上のように決めると、関数抽象 (fun x -> e) と関数適用 (e1 e2) の評価方法は比較的容易に実装することができる。 新しいインタープリタを eval4 とする。
(* eval4 : env -> (string * value) list -> value *) (* ラムダ式の導入 *) let rec eval4 e env = let binop f e1 e2 env = ... in match e with ... | Fun(x,e1) -> FunVal(x, e1, env) | App(e1,e2) -> (match (eval4 e1 env) with FunVal(x,body,env1) -> let arg = (eval4 e2 env) in eval4 body (ext env1 x arg) | _ -> failwith "function value expected") | _ -> failwith "unknown expression"まず、Fun(x,e1)の形の式のときは、上記の解説通り、現在の環境 env を 関数クロージャに取りこんで返す。ここで、関数定義の本体である e1 は 全く計算せず、そのまま取りこむことに注意せよ。
次に、(e1 e2) の形の関数適用の処理である。これが一番面白いところである。 この処理は、以下の手順で行われる。
let x = 1 in let f = fun y -> x + y in let x = 2 in f (x + 3)
(print_string "1"; 10) + (print_string "2"; 20)このプログラムは、プリント文をのぞけば 10 + 20 を計算しているだ けであるが、問題は、2つのプリント文のどちらが先に計算されるか、である。 OCaml 処理系では、"1" と "2" のどちらが先に印刷されるかを確かめよ。
上記と同様に、関数の適用においても、OCaml は(予想外の)順序で評価を行う。 たとえば、
(print_string "1"; (fun x -> x)) (print_string "2"; 20)を実行してみよ。
この実験で与えた eval4 が、e1+e2 や e1 e2 という式の計算を、どういう順序で行うか答えなさい。 また、OCaml と食い違っている順序になっている部分は、OCaml と一致させる ようにeval4 を変更しなさい。 (ただし、ここまでで作成した eval4 にはプリント文など副作用を持つ式が含 まれていないので、評価順序の食い違いは表面化しない。将来、プリント文 や例外処理を導入したときに表面化する。)
[補足] この問題において、上記の OCamlプログラムを真似して、以下のプログラムを eval4 に渡するのは意味がない。
eval4 (App((print_string "1"; Fun("x",Var "x")), (print_string "2"; IntLit 100))) (emptyenv ())これでは、第1引数が eval4 に渡される前に、 print_string "1" と print_string "2" が実行されてしまうので、 eval4 の評価順序を調べることにならず、OCamlの評価順序を調べている ことになる。従って、上記のプログラムについては、OCaml と eval4 の結果 は一致してしまう。
亀山 幸義