鍍金池/ 教程/ Scala/ 函數–尾遞歸
包對象
Ordered Trait
組合和繼承–定義 final 成員
基本數據類型
Match 表達式
類和對象 (三)
操作基本數據類型
for 表達式
組合和繼承–重載成員函數和方法
類和對象 (二)
組合和繼承–定義 factory 對象
組合和繼承–多態(tài)和動態(tài)綁定
Trait 的基本概念
if 表達式
組合和繼承–抽象類
函數–函數字面量的一些簡化寫法
while 循環(huán)
組合和繼承–使用組合還是繼承?
訪問控制修飾符
Trait 示例–Rectangular 對象
組合和繼承–定義參數化成員變量
組合和繼承–定義無參數方法
類和對象 (一)
函數–閉包
函數–類成員函數
Scala 基本數據類型的實現方法
try 表達式處理異常
選擇瘦接口還是胖接口設計?
組合和繼承–小結
創(chuàng)建新的控制結構
使用 import
為訪問控制修飾符添加作用域
Scala 的類層次關系
類和對象 (五)
傳名參數
柯里化函數
函數–頭等公民
組合和組合和繼承–定義 heighten 和 widen 函數
使用 Package–將代碼放入包中
隱含的 import
所有類的公共子類–底層類型
進一步 Scala
函數–局部函數
引用包中的代碼
組合和繼承–使用 override 修飾符
組合和繼承–實現類 Element 的 above,beside 和 toString()方法
類和對象 (四)
函數–尾遞歸
沒有“break”和“continue”的日子
組合和繼承–調用基類構造函數
減低代碼重復
函數–函數–可變參數,命名參數,缺省參數
起步 Scala
組合和繼承–擴展類
函數–部分應用的函數
開始神奇的 Scala編程之旅
組合和繼承–概述
Trait 用來實現可疊加的修改操作

函數–尾遞歸

在前面的文章中我們提到過可以使用遞歸函數來消除需要使用 var 變量的 while 循環(huán)。下面為一個使用逼近方法求解的一個遞歸函數表達:

def approximate(guess: Double) : Double =
  if (isGoodEnough(guess)) guess
  else approximate(improve(guess))

通過實現合適的 isGoodEnough 和 improve 函數,說明這段代碼在搜索問題中經常使用。 如果你打算 approximate 運行的快些,你很可能使用下面循環(huán)來實現什么的算法:

def approximateLoop(initialGuess: Double) : Double = {
  var guess = initialGuess
  while(!isGoodEnough(guess))
    guess=improve(guess)
  guess
 } 

那么這兩種實現哪一種更可取呢? 從簡潔度和避免使用 var 變量上看,使用函數化編程遞歸比較好。但是有 whil e循環(huán)是否運行效率更高些?實際上,如果我們通過測試,兩種方法所需時間幾乎相同,這聽起來有些不可思議,因為回調函數看起來比使用循環(huán)要耗時得多。

其實,對于 approximate 的遞歸實現,Scala 編譯器會做些優(yōu)化,我們可以看到 approximate 的實現,最后一行還是調用 approximate 本身,我們把這種遞歸叫做尾遞歸。Scala 編譯器可以檢測到尾遞歸而使用循環(huán)來代替。因此,你應該習慣使用遞歸函數來解決問題,如果是尾遞歸,那么在效率時不會有什么損失。

跟蹤尾遞歸函數

一個尾遞歸函數在每次調用不會構造一個新的調用棧(stack frame)。所有遞歸都在同一個執(zhí)行棧中運行,如果你在調試時,如果在尾遞歸中調試錯誤,不會看到嵌套的調用棧,比如下面的代碼,非尾遞歸的一個實現:

scala> def boom(x:Int):Int=
     |   if(x==0) throw new Exception("boom!") else boom(x-1) + 1
boom: (x: Int)Int
scala> boom(5)
java.lang.Exception: boom!
  at .boom(<console>:8)
  at .boom(<console>:8)
  at .boom(<console>:8)
  at .boom(<console>:8)
  at .boom(<console>:8)
  at .boom(<console>:8)
  ... 32 elided

boom 不是一個尾遞歸,因為最后一行為一個遞歸加一操作,可以看到調用 boom(5)的調用棧,為多層。

我們修改這段代碼,使它構成一個尾遞歸:

scala> def bang(x:Int):Int=
     |        if(x==0) throw new Exception("boom!") else bang(x-1)
bang: (x: Int)Int
scala> bang(5)
java.lang.Exception: boom!
  at .bang(<console>:8)
  ... 32 elided

這一次,只看到一層調用棧,Scala 編譯器將尾遞歸優(yōu)化從循環(huán)實現,如果你不想使用這個特性,可以添加 scalac 編譯參數 -g:notailcalls 來取消這個優(yōu)化,此后,如果拋出異常,尾遞歸也會顯示多層調用棧。

尾遞歸的一些局限性

目前來說,Scala 編譯器只能對那些直接實現尾遞歸的函數,比如前面的 approximate 和 bang,如果一個函數間接實現尾遞歸,比如下面代碼:

def isEven(x:Int): Boolean =
  if(x==0) true else isOdd(x-1)
def isOdd(x:Int): Boolean=
  if(x==0) false else isEven(x-1)

isEven 和 isOdd 事件也是為遞歸,但不是直接定義的尾遞歸,scala 編譯器無法對這種遞歸進行優(yōu)化。