さて、次は関数型言語ではループのかわりによく使われるともっぱらの噂?の再帰についてみてみようと思います。まぁ関数型言語じゃなくても再帰は一般的に使われてるけど、でかいデータを相手にしたときにスタックオーバーフロー起こすから気を付けようねって感じだと思います。
とりあえず単純にリスト内の要素の総和を求める関数を再帰を使って書いてみようと思います。
// 普通に再帰 let rec sum1 l = match l with | head::tail -> head + sum1(tail) | [] -> 0 // 実行してみる let list = [1..100] printfn "%d" <| sum1 list
パターンマッチのhead::tailは、リストに対するパターンマッチでheadに最初の要素tailに2つめ以降の要素が入ります。そして[]は空のリストにマッチするパターンです。あと、再帰呼び出しをする関数にはrecというキーワードもつけないといけない点が特徴的ですね。
やってることは簡単で、リストの最初の要素に対して残りの部分の総和を足すということを再帰的にやって、最後にリストの要素がなくなったら0を返すという一般的な再帰のパターンだと思います。実行してみると以下のような結果になります。
5050
さて、1〜100の和くらいならよかったのですが、ちょっと大きなリストの和を求めてもらうとどうなるでしょう。試してみます。数が大きいのでintではなくてint64にしました。
// 普通に再帰 let rec sum1 l = match l with | head::tail -> head + sum1(tail) | [] -> 0L // 実行してみる let list = [1L..100000L] printfn "%d" <| sum1 list
1〜10万です、結構おおきな数字ですね。実行すると以下のようになります。
Process is terminated due to StackOverflowException.
スタックオーバーフローになってしまいました。
F#には、この問題を解決するために末尾再帰と呼ばれるものがあります。このsumの例だと再帰が最後に行くまで計算結果が確定しないのでスタックに計算途中の値を積み上げないといけないのですが、これをなくしてやろうというアプローチです。どうやるのかというと、計算の経過を関数の引数として持ちまわってやろうというアプローチになります。
ということでsumを書き換えてみます。
// 末尾再帰 let sum2 l = // accに計算経過いれて引数で持回る let rec sumLoop acc list = match list with | head::tail -> sumLoop (acc + head) tail | [] -> acc // 計算経過を0(初期値)でsumLoopを呼び出す sumLoop 0L l // 実行してみる let list = [1L..100000L] printfn "%d" <| sum2 list
さて、実行してみましょう。
5000050000
あってるのかどうかわかりませんが、スタックオーバーフローにならずに計算結果が出ました。これは裏でF#コンパイラが空気を読んで再帰呼び出しをループに最適化してるのだと思われます。(逆コンパイルして確認したわけではないので予想です)
要は、自分で最初からループで書いても同じですね。
// ループ let sum3 l = let sum = ref 0L for i in l do sum := !sum + i !sum // 実行してみる let list = [1L..100000L] printfn "%d" <| sum3 list
ただ、ループを書くと副作用のある変数を作ったりあまり関数型チックとは言えないのと、再帰的に扱えるものは再帰で書いたほうがすっきりするので、末尾再起になるように心がけて書いていこうと思った今日この頃でした。
過去記事
- 手軽なスクリプト言語としてのF#
- 手軽なスクリプト言語としてのF# その2
- 手軽なスクリプト言語としてのF# その3
- 手軽なスクリプト言語としてのF# その4
- 手軽なスクリプト言語としてのF# その5
- 手軽なスクリプト言語としてのF# その6
- 手軽なスクリプト言語としてのF# その7
- 手軽なスクリプト言語としてのF# その8「レコード」
- 手軽なスクリプト言語としてのF# その9「クラス」
- 手軽なスクリプト言語としてのF# その10「継承・アブストラクトクラス」
- 手軽なスクリプト言語としてのF# その11「インターフェースと演算子のオーバーロード」
- 手軽なスクリプト言語としてのF# その12「ラムダ式とイベント」
- 手軽なスクリプト言語としてのF# その13「オブジェクト初期化子みたいなの」
- 手軽なスクリプト言語としてのF# その14「合成演算子とパイプ演算子」
- 手軽なスクリプト言語としてのF# その15「WPFしてみた」
- 手軽なスクリプト言語としてのF# その16「総称型 ジェネリック」
- 手軽なスクリプト言語としてのF# その17「リスト」
- 手軽なスクリプト言語としてのF# その18「オプション型」
- 手軽なスクリプト言語としてのF# その19「参照型よりオプション型って安全?」
- 手軽なスクリプト言語としてのF# その20「パターンマッチ」