先日、関数型まつりに参加した影響で、久しぶりにフィボナッチ数を求める関数でも書くか!と思って書き始めたら、再帰+末尾最適化バージョンが一発でそらで書けず、過去に書いたコードを見て、「あー」となったので、その記録。
なんでも忘れるものですね...
単純なループバージョン
定義の通りに書いただけ
でも再帰を使ってないので、関数型っぽくない
def fib1(n: Int): BigInt = if n <= 1 then BigInt(n) else var a: BigInt = 0 var b: BigInt = 1 for (_ <- 2 to n) val n = a + b a = b b = n b
素直に再帰を使って書いたバージョン
これも定義通りに素直に書いただけ
def fib2(n: Int): BigInt = def loop(n: Int): BigInt = if n <= 1 then BigInt(n) else loop(n - 2) + loop(n - 1) loop(n)
でも、末尾最適化が効かないため、処理時間は爆増する
fib2(50)
くらいで全然帰ってこなくなる
末尾最適化に対応したバージョン
過去のコードを見て書き写した...
ここでのloop
関数の中でのcount
は、単純な再帰バージョンでのインデックスではなく、カウントダウンするためのカウンター
acc
の中に、累積した値を持ち回り、計算完了時点(count
がゼロになった時点)で、acc
を取得する
def fib3(n: Int): BigInt = @annotation.tailrec def loop(count: Int, acc: (BigInt, BigInt)): BigInt = if count <= 0 then acc._1 else loop(count - 1, (acc._2, acc._1 + acc._2)) loop(n, (0, 1))
これなら、fib3(50)
は当然余裕で帰ってくる
カウンターが下がっていくのと逆に、引数で持ち回っているacc
がどんどん累積されて増えていく、という感覚が掴めるか?というところがポイントですね