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 になってしまい、 上記のプログラムを OCamlで実行したときの値 6 と異なってしまうからである。
上記のプログラムを正しく実行するためには、 「f に束縛される値は (fun y -> x + y) という式であるが、 そこでの x の値が 1 である」という風に、x の値の情報を持っておく必要がある。 なるほど! このように、計算結果として関数が返るときは(上記の let f = ... jの ....において、fun y->x+y という式は、計算されて、関数を返している)、 関数そのものだけでなく、その時点での環境をセットにして保存する必要がある。
この考えに基づき、値の型の定義を以下のように改善する。
(* 値の型 *) type value = | IntVal of int | BoolVal of bool | FunVal of string * exp * ((string * value) list)前回に比べて、FunVal の引数の型に (string * value) list が加わった。 これは、環境の型であった。
冒頭のプログラムについて考えると、(fun y -> x + y) という式を計算した結果としての値は、 FunVal ("x", Plus(...)) ではなく、 FunVal ("x", Plus(...), [("x",IntVal(1))]) となる。 ここで [("x",IntVal(1))] は、(fun y -> x + y) を計算している時の環境であり、 (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) -> begin 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" end | _ -> failwith "unknown expression"まず、Fun(x,e1)の形の式のときは、上記の解説通り、現在の環境 env を 関数クロージャに取りこんで返す。ここで、関数定義の本体である e1 は 全く計算せず、そのまま取りこむことに注意せよ。
次に、App(e1,e2) の形の関数適用の処理である。これが一番面白いところである。 この処理は、以下の手順で行われる。
let x = 1 in let f = fun y -> x + y in let x = 2 in f (x + 3)
(print_string "point-1"; 10) + (print_string "point-2"; 20)問題は、2つのプリント文のどちらが先に計算されるか、である。 OCaml 処理系では、"point-1" と "point-2" のどちらが先に印刷されるかを確かめよ。
上記と同様に、関数の適用においても、OCaml は(予想外の)順序で評価を行う。 たとえば、
(print_string "point-1"; (fun x -> x)) (print_string "point-2"; 20)を実行してみよ。
[補足] この問題において、上記の 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 の結果 は一致してしまう。
亀山 幸義