尾遞迴

使用常規遞迴,每個遞迴呼叫將另一個條目推送到呼叫堆疊。遞迴完成後,應用程式必須將每個條目一直彈回。如果有很多遞迴函式呼叫,它最終會有一個巨大的堆疊。

如果在尾部位置找到遞迴呼叫,Scala 會自動刪除遞迴。可以將註釋(@tailrec)新增到遞迴函式以確保執行尾呼叫優化。如果編譯器無法優化你的遞迴,則會顯示錯誤訊息。

定期遞迴

這個例子不是尾遞迴的,因為當進行遞迴呼叫時,函式需要跟蹤呼叫返回後它需要對結果進行的乘法。

def fact(i : Int) : Int = {
   if(i <= 1) i
   else i * fact(i-1)
}

println(fact(5))

使用引數呼叫函式將產生如下所示的堆疊:

(fact 5)
(* 5 (fact 4))
(* 5 (* 4 (fact 3)))
(* 5 (* 4 (* 3 (fact 2))))
(* 5 (* 4 (* 3 (* 2 (fact 1)))))
(* 5 (* 4 (* 3 (* 2 (* 1 (fact 0))))))
(* 5 (* 4 (* 3 (* 2 (* 1 * 1)))))
(* 5 (* 4 (* 3 (* 2))))
(* 5 (* 4 (* 6)))
(* 5 (* 24))
120

如果我們嘗試使用 @tailrec 註釋此示例,我們將收到以下錯誤訊息:could not optimize @tailrec annotated method fact: it contains a recursive call not in tail position

尾遞迴

在尾遞迴中,首先執行計算,然後執行遞迴呼叫,將當前步驟的結果傳遞給下一個遞迴步驟。

def fact_with_tailrec(i : Int) : Long = {
   @tailrec
   def fact_inside(i : Int, sum: Long) : Long = {
      if(i <= 1) sum
      else fact_inside(i-1,sum*i)
   }
   fact_inside(i,1)
}

println(fact_with_tailrec(5))

相反,尾遞迴階乘的堆疊跟蹤如下所示:

(fact_with_tailrec 5)
(fact_inside 5 1)
(fact_inside 4 5)
(fact_inside 3 20)
(fact_inside 2 60)
(fact_inside 1 120)

每次呼叫 fact_inside 時,只需要跟蹤相同數量的資料,因為該函式只是將它所獲得的值返回到頂部。這意味著即使呼叫 fact_with_tail 1000000,它也只需要與 fact_with_tail 3 相同的空間量。非尾遞迴呼叫不是這種情況,因此大值可能導致堆疊溢位。