如果一個函式最後執行的動作是呼叫它自己,這個函式就是尾端遞迴。
每當任何函式被呼叫,一個帶有區域變數與引數的堆疊框架就會被放到函式呼叫堆疊的最上方。 當函式回傳時,這個堆疊框架就會從堆疊中移除。
尾端遞迴函式可以進行尾呼叫最佳化(或稱尾呼叫消除)。 這種最佳化讓下一次函式呼叫能重複使用上一個堆疊框架,前提是前一個函式確定已經不再需要它了。 這能減輕函式呼叫堆疊溢位的問題:當函式呼叫堆疊上的框架多到沒有記憶體能再建立另一個框架時,就會發生這種情況。
在某些條件下,Elm 編譯器在編譯成 JavaScript 時,能夠自動進行尾呼叫最佳化。
當一個分支中最後執行的操作,是以單純的函式呼叫來呼叫函式本身時,遞迴函式就能套用這項最佳化。 讓我們來看幾個例子:
factorial : Int -> Int
factorial n =
if n <= 1 then
n
else
n * factorial (n-1)
上面的實作並不是尾端遞迴,因為 else 分支中最後的操作是乘法 n *。
factorial : Int -> Int
factorial n =
factorialHelper n n
factorialHelper : Int -> Int -> Int
factorialHelper n resultSoFar =
if n <= 1 then
resultSoFar
else
factorialHelper (n-1) (n * resultSoFar)
上面的實作是尾端遞迴,而且會被最佳化,因為 else 分支中最後的操作是 factorialHelper 呼叫自己。
對於型別簽章為 Int -> Int 的函式來說,這是不可能做到的,因此實務上尾呼叫最佳化通常是靠定義輔助函式來達成。
Piper 是個熱愛烤派的人。
沒有人知道她是因為自己的名字才做起派來,還是為了配合這項興趣才改了名字。 乍看之下,後者似乎不太可能,可是你知道嗎,Piper 對派簡直著迷得不得了。 她總在廚房裡東摸西摸,調整食譜、精進手藝,朋友們看了都開心極了。
她最近迷上什麼呢? 就是烤出盡可能圓的派,圓到數學上完美的境界,而且還借助她最愛的數字,你猜到了:π。
Piper 找到了一個很棒的公式,可以用疊代的方式計算 π,那就是牛頓/歐拉收斂轉換:
π / 2 = sum for k from 0 to infinity of ( k! ) / ( 2 * k + 1 )!!
一起來計算 π,幫 Piper 烤出數學上完美的派吧。
我們先來暖身一下。
階乘運算子通常寫成 !,定義如下
0! = 1
n! = 1 * 2 * 3 * ... * n
定義factorial函式,以尾遞迴的方式計算階乘。
factorial 4
-- 24
雙階乘運算子通常寫成 !!,定義如下
0!! = 1
n!! = 1 * 3 * 5 * ... * n (for odd n)
n!! = 2 * 4 * 6 * ... * n (for even n)
定義doubleFactorial函式,以尾遞迴的方式計算雙階乘。
factorial 5
-- 15
factorial 6
-- 48
定義pipersPi函式,以尾遞迴的方式,取牛頓/歐拉收斂轉換公式中固定數量的項來逼近 π。
π / 2 = sum for k from 0 to infinity of ( k! ) / ( 2 * k + 1 )!!
我們一起來算算第一項吧。
當上限是 0(而不是無限大)時,我們會得到:
π / 2 ≈ Sum for k from 0 to 0 of ( k! ) / ( 2 * k + 1 )!!
π / 2 ≈ ( 0! ) / ( 2 * 0 + 1 )!!
π / 2 ≈ 0! / 0!!
π / 2 ≈ 1 / 1
π / 2 ≈ 1
π ≈ 2
每多一項,近似值就會更精確。
pipersPi 0
-- 2.0
pipersPi 1
-- 2.6666666