(* eval2 : exp -> value *) let rec eval2 e = match e with IntLit(n) -> IntVal(n) | Plus(e1,e2) -> (match (eval2 e1, eval2 e2) with (IntVal(n1),IntVal(n2)) -> IntVal(n1+n2) | _ -> failwith "Plus for non-integer value(s)") | Times(e1,e2) -> (match (eval2 e1, eval2 e2) with (IntVal(n1),IntVal(n2)) -> IntVal(n1*n2) | _ -> failwith "Times for non-integer value(s)") | _ -> failwith "unknown expression"これでも動くのであるが、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"eval2, eval2a, eval2bはまったく同じ動作をする関数であるが、eval2b は その中で binop という補助的な関数を定義していて、それを使って、 同じような処理をまとめてコンパクトに書いている。 このような処理をきれいに書けるところも OCamlならでは、である。
eval2a との違いは、 eval2b という関数の中で、binop という関数を定義してい る点である。 このようにすることにより、eval2b の外からは、binop という関 数は使えない。従って、他の関数として binop というものがあっても名前が 衝突しない、というメリットがある。
もう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なんと複雑な! と思ったかもしれないが、そんな面倒な型は、処理系が勝手に 導出してくれるので気にしなくてよく、ここでは、「関数を引数として、別の 関数に渡すことができる」という事実に着目しよう。(このような関数は、 普通の関数よりちょっと「偉い」ので、高階関数 (higher-order function)と呼 ばれる。実は、高階関数を自由自在に使えるという事が、 関数型言語の最大の特徴である。)
もっとも、binop を書くためには、高階関数を絶対使わないといけないわけで はなく、他の方法もたくさんある。ただ、ここで言いたいのは、高階関数とい う概念は、普通にプログラムを「美しく簡潔に」書こうと思うと、極めて 自然に出てくるものだ、ということだ。いずれ、高階関数が書けないプログラム言語 なんて、とてもやってられない、と思うようになれば、関数型プログラマとなっ たと言えよう。
さらに寄り道だが、足し算の関数は (+) と書いたが、かけ算の関数は (*) で はなく、( * ) である。なぜなら、OCaml ではコメントを書くために (* ... というパターンを使ってしまったから、かけ算の関数を表すために 無駄にスペースをいれないといけなくなった、という、些細な理由からである。
さて、上記の点を除いては、binop の定義には何も不思議なことはなく、 eval2 において Plus と Times でやっていた処理を、binop にまとめた だだけである。なお、binop は再帰関数ではないので、let rec binop ... と 書く必要はない。
亀山 幸義