「プログラミング in OCaml ~関数型プログラミングの基礎からGUI構築まで~」を読んでの勉強記録です。
再帰関数の定義ではletの後にrecを付ける。コメントは(**)で囲む。
# let rec sum n = (* 1からnまでの合計 *) if n = 0 then 0 else n + sum (n - 1);; val sum : int -> int = <fun> # sum 10;; - : int = 55 # let rec fib n = (* n項目のフィボナッチ数を求める *) if n = 1 || n = 2 then 1 else fib (n - 1) + fib (n - 2);; val fib : int -> int = <fun> # fib 7;; - : int = 13
関数内で変数や関数を定義できる。
# let fib n = (* 改良版fib *) let rec fib' n = if n = 1 then (1, 0) else let (y, x) = fib' (n - 1) in (y + x, y) in let (a, _) = fib' n in a;; val fib : int -> int = <fun>
再帰させる場合はrecが必要というのが変わっています。もともとlet定義は左辺の変数を右辺では使えないので、再帰的に使えるようにするためにrecで明示するというのは筋が通っている気がします。
末尾再帰の最適化により、末尾呼び出しのみの関数は一定量以上のスタックを消費しない。
# let rec tail_sum1 n a = (* 末尾再帰sumその1。nから1までaに足しこむ *) if n = 0 then a else tail_sum1 (n - 1) (a + n);; val sum : int -> int -> int = <fun> # let tail_sum2 n = (* 末尾再帰sumその2。1からnまでaに足しこむ *) let rec tail_sum2' i a = if i > n then a else tail_sum2' (i + 1) (a + i) in tail_sum2' 1 0;; val tail_sum2 : int -> int = <fun> # sum 1000000;; Stack overflow during evaluation (looping recursion?). # tail_sum1 1000000 0;; - : int = -363189984 # tail_sum2 1000000;; - : int = -363189984
sumではスタックオーバフローの例外が出ていますが、2つのtail_sumはしっかり計算しています(ただし、数字が大きくなりすぎて整数オーバーフローが起きています)。また、tail_sum2'では外側のtail_sum2の引数であるnを自身の引数であるiとの比較で使用している点にも注意が必要です。
通常の再帰関数を末尾再帰にするポイントは答えの途中経過を関数の引数にすることです。sumでは再帰的なsumの結果とその時点でのnを加算するために、再帰呼び出し時のコンテキストをスタックに格納します。一方、tail_sumでは引数aとしてそれまでの結果を渡しているので再帰呼び出しの結果とその時点でのnとの計算をしないで済みます。このためスタックに呼び出し前の状態を保存しておく必要が無くなります。これが末尾再帰の最適化というものらしいです。今まで名前だけは聞いてきましたが、やっとどういうものか理解できました。
今回はここまで。次回はカリー化、高階関数、匿名関数についてです。