(* eval2 : exp -> value *) let rec eval2 e = match e with | IntLit(n) -> IntVal(n) | Plus(e1,e2) -> begin match (eval2 e1, eval2 e2) with | (IntVal(n1),IntVal(n2)) -> IntVal(n1+n2) | _ -> failwith "integer values expected" end | Times(e1,e2) -> begin match (eval2 e1, eval2 e2) with | (IntVal(n1),IntVal(n2)) -> IntVal(n1*n2) | _ -> failwith "integer values expected" end | _ -> failwith "unknown expression e"Plus と Times の処理はほとんど同じであることが一目瞭然である。 更に、引き算や割り算などの演算を追加したら、似たような記述が何回も出現することになる。 そのようなプログラムの作成は面倒だし、また、コピーして作ってしまったら、 後でプログラムの変更をしたいときも、たくさんの箇所を直さないといけないので手間がかかるし、間違いやすくなる。 (「プログラムの保守性が悪くなる」という言いかたをする。)
何よりも、 この実験は、美しく、簡潔にプログラミングしよう! という趣旨であるので、このような冗長な書き方は許しがたい。 というわけで、見た目の美しさを追求して、上記の部分を1つにまとめることにする。 (なお、以下のように変更しても、実行効率はまったく改善されない。あくまで、プログラムの保守性の問題である。)
最初に思いつくのは、処理をまとめて1つの関数として定義することである。 ここでは binop (binary operator の処理、という意味)という名前の関数とする。
(* eval2a : exp -> value *) (* binop : int -> exp -> evp -> value *) let rec eval2a e = match e with | IntLit(n) -> IntVal(n) | Plus(e1,e2) -> binop 1 e1 e2 | Times(e1,e2) -> binop 2 e1 e2 | _ -> failwith "unknown expression" and binop flag e1 e2 = match (eval2a e1, eval2a e2) with | (IntVal(n1),IntVal(n2)) -> if flag = 1 then IntVal(n1 + n2) else IntVal(n1 * n2) | _ -> failwith "integer values expected"ここで、let rec ... and .... という構文は、 eval2a と binop という2つの関数を同時に定義するための構文である。 上記のケースでは、eval2a から binop を呼び、binop から eval2a を呼ぶ、という 相互再帰的な関数であるので、関数を1つずつ順番に定義することはできず、同時定義の構文を使った。
関数 binop は第1引数が 1 であるか 2 であるかにより、 足し算とかけ算を切り換えて計算する。 上記のような変形により、割り算や引き算などの2項演算子の処理の追加が容易にできるようになる。
これで一応目的は達したが、ついでに、「OCaml らしいプログラム」の書き方をしてみよう。 下記のプログラムになると、かなり「OCaml の味」が出た書き方になる。
(* eval2b : exp -> value *) let rec eval2b e = let binop f e1 e2 = match (eval2b e1, eval2b e2) with | (IntVal(n1),IntVal(n2)) -> IntVal(f n1 n2) | _ -> failwith "integer values expected" in match e with | IntLit(n) -> IntVal(n) | Plus(e1,e2) -> binop (+) e1 e2 | Times(e1,e2) -> binop ( * ) e1 e2 | _ -> failwith "unknown expression"eval2a との違いは、 eval2b という関数の「中で」、binop という関数を定義している点である。 (eval2a では、binop は eval2a と同時再帰的に定義されていた。) このようにすることにより、eval2b の外から binop という関数は使えない。 従って、他に binop という名前の関数があっても名前が衝突しない、というメリットがある。 (オブジェクト指向言語のカプセル化に類似。)
eval2b には、もう1つ、興味深い点がある。 eval2b の本体から binop という関数を呼ぶときに、 Plus のときと Times のときとで異なる処理をするよう依頼するが、 その情報を binop に対して、「関数」として渡している。 すなわち、上のプログラムで (+) は、「足し算を意味する記号」ではなく、 「足し算をする関数」である(!) 試しに以下のプログラムを実行してみよ。
(+) 1 2通常は 1+2 と書いて足し算を表現するが、+ の記号のまわりに「かっこ」を つけて (+) とすることにより、(+) 1 2 の形で足し算を表している。 しかし、もっと驚くことは、関数 binop に対する引数として、この「足し算 をする関数」を与えていることである(!)
| Plus(e1,e2) -> binop (+) e1 e2ここでわかるように、binop の第1引数は、整数や exp型の式ではなく、関数である。 実際 binop の型は、
(int -> int -> int) -> exp -> exp -> valueこれは、第1引数が int -> int -> int という型を持つ関数であることを示し ている。
注1. binop を書くためには、高階関数を絶対使わないといけないわけではなく、 他の方法もたくさんある。ただ、ここで言いたいのは、高階関数とい う概念は、普通にプログラムを「美しく簡潔に」書こうと思うと、 極めて自然に出てくるものだ、ということだ。 皆さんも、 いつか「高階関数が書けないプログラム言語なんて、とてもやってられない」と思うだろうが、 そうなれば、関数型プログラマとなったと言えよう。
注2. 足し算の関数は (+) と書いたが、かけ算の関数は (*) ではなく、 スペースをあけて、( * ) と書いている。これは何だろう? 実は、OCaml ではプログラム中に注釈を書くために (* ... *) というパターンを使う。 そこで、(*)と書くと、注釈の始まりと解釈されてしまうので、 かけ算の関数を表すためには無駄にスペースをいれないといけなくなった。
さて、上記の点を除いては、binop の定義には何も不思議なことはなく、 eval2 において Plus と Times でやっていた処理を、 binop にまとめただだけである。binop は再帰関数ではないので、 let rec binop ... と書く必要はない。
亀山 幸義