開發軟件時首先要看的是代碼的可讀性和可維護性。查看性能特徵大多是不成熟的優化。
沒有理由不使用遞歸來幫助編寫高質量的代碼。
尾部遞歸與正常循環相同。看看這個簡單的尾遞歸函數:
def gcd(a: Int, b: Int) = {
def loop(a: Int, b: Int): Int =
if (b == 0) a else loop(b, a%b)
loop(math.abs(a), math.abs(b))
}
它計算兩個數字的最大公約數。一旦你知道這個算法,它很清楚它是如何工作的 - 用while循環寫這個並不會讓它更清晰。相反,您可能會在第一次嘗試時引入錯誤,因爲您忘記將新值存儲到變量a
或b
之一。
在另一邊看到這兩個函數:
def goRec(i: Int): Unit = {
if (i < 5) {
println(i)
goRec(i+1)
}
}
def goLoop(i: Int): Unit = {
var j = i
while (j < 5) {
println(j)
j += 1
}
}
哪一個更容易閱讀?它們大致相同 - 由於基於Scalas表達式的性質,您獲得的用於尾遞歸函數的所有語法都消失了。
對於遞歸函數還有另外一件事情要做:懶惰評估。如果您的代碼是懶惰評估它可以遞歸,但不會發生堆棧溢出。看到這個簡單的功能:
def map(f: Int => Int, xs: Stream[Int]): Stream[Int] = f -> xs match {
case (_, Stream.Empty) => Stream.Empty
case (f, x #:: xs) => f(x) #:: map(f, xs)
}
它會崩潰的大輸入?我不這麼認爲:
scala> map(_+1, Stream.from(0).takeWhile(_<=1000000)).last
res6: Int = 1000001
試圖與Scalas List
同樣會殺了你的程序。但因爲Stream
是懶惰這不是一個問題。在這種情況下,你也可以編寫一個尾遞歸函數,但通常這是不容易的。
有許多算法在迭代編寫時不會清晰 - 一個示例是圖的depth first search。你是否想自己維護一個堆棧來保存你需要返回的值?不,你不會,因爲它容易出錯並且看起來很醜陋(除了遞歸定義之外 - 它也會調用迭代深度優先搜索遞歸,因爲它必須使用堆棧,而「正常」遞歸必須使用堆棧而且它只是隱藏在開發者面前並由編譯器維護)。
回來過早優化的點,我聽到一個很好的比喻:當你不能用Int
要解決的問題,因爲你的號碼將獲得大,很可能是你溢出則不會切換到Long
,因爲它很可能也會在這裏發生溢出。
對於遞歸,這意味着可能會出現堆棧炸燬的情況,但更有可能是當您切換到基於內存的解決方案時,您將會遇到內存不足錯誤。更好的建議是找到一個不能很好地執行的算法。
作爲結論,嘗試優先選擇尾遞歸而不是循環或正常遞歸,因爲它肯定不會殺死你的棧。但是當你能做得更好時,不要猶豫,做得更好。
+1對於貓。問題也很有趣。 – giampaolo