此第一版是為 Lua 5.0 編寫的。儘管在很大程度上仍然適用於後續版本,但仍存在一些差異。
第四版針對 Lua 5.3,可在 Amazon 和其他書店購買。
購買此書,您還將幫助 支持 Lua 項目


6.3 – 正確的尾部呼叫

Lua 中函數的另一個有趣特徵是它們執行正確的尾部呼叫。(儘管該概念不直接涉及遞歸,但許多作者都使用術語正確的尾部遞歸。)

尾部呼叫是一種偽裝成呼叫的 goto。當函數將另一個函數作為其最後一個動作呼叫時,就會發生尾部呼叫,因此它無需執行其他任何操作。例如,在以下代碼中,對 g 的呼叫是一個尾部呼叫

    function f (x)
      return g(x)
    end
f 呼叫 g 之後,它無需執行其他任何操作。在這種情況下,當被呼叫函數結束時,程序無需返回呼叫函數。因此,在尾部呼叫之後,程序無需在堆疊中保留任何關於呼叫函數的信息。一些語言實現(如 Lua 解釋器)利用了這一事實,並且在執行尾部呼叫時實際上不使用任何額外的堆疊空間。我們說這些實現支持正確的尾部呼叫

由於正確的尾部呼叫不使用堆疊空間,因此程序可以執行的「嵌套」尾部呼叫的數量沒有限制。例如,我們可以用任何數字作為參數呼叫以下函數;它永遠不會溢出堆疊

    function foo (n)
      if n > 0 then return foo(n - 1) end
    end

在我們使用正確的尾部呼叫時,一個微妙的觀點是尾部呼叫是什麼。一些明顯的候選者不符合呼叫函數在呼叫後無需執行的任何操作的標準。例如,在以下代碼中,對 g 的呼叫不是尾部呼叫

    function f (x)
      g(x)
      return
    end
該示例中的問題是,在呼叫 g 之後,f 在返回之前仍然必須捨棄來自 g 的偶爾結果。同樣,以下所有呼叫都不符合標準
    return g(x) + 1     -- must do the addition
    return x or g(x)    -- must adjust to 1 result
    return (g(x))       -- must adjust to 1 result
在 Lua 中,只有 return g(...) 格式的呼叫才是尾部呼叫。但是,g 及其參數都可以是複雜表達式,因為 Lua 在呼叫之前會對它們求值。例如,下一個呼叫是一個尾部呼叫
      return x[i].foo(x[j] + a*b, i + j)

正如我之前所說,尾部呼叫是一種 goto。因此,正確的尾部呼叫在 Lua 中的一個非常有用的應用是編程狀態機。此類應用程序可以用函數表示每個狀態;更改狀態就是轉到(或呼叫)特定函數。舉例來說,我們考慮一個簡單的迷宮遊戲。迷宮有幾個房間,每個房間最多有四個門:北、南、東和西。在每一步中,用戶輸入一個移動方向。如果在該方向上有一扇門,則用戶轉到相應的房間;否則,程序將打印一個警告。目標是從初始房間移動到最終房間。

此遊戲是一個典型的狀態機,其中目前的房間為狀態。我們可以使用每個房間的一個函數來實作此迷宮。我們使用尾呼叫從一個房間移動到另一個房間。一個有四個房間的小迷宮可能如下所示

    function room1 ()
      local move = io.read()
      if move == "south" then return room3()
      elseif move == "east" then return room2()
      else print("invalid move")
           return room1()   -- stay in the same room
      end
    end
    
    function room2 ()
      local move = io.read()
      if move == "south" then return room4()
      elseif move == "west" then return room1()
      else print("invalid move")
           return room2()
      end
    end
    
    function room3 ()
      local move = io.read()
      if move == "north" then return room1()
      elseif move == "east" then return room4()
      else print("invalid move")
           return room3()
      end
    end
    
    function room4 ()
      print("congratulations!")
    end
我們以呼叫初始房間來開始遊戲
    room1()
沒有適當的尾呼叫,每個使用者的移動都會建立一個新的堆疊層級。在一些移動之後,將會發生堆疊溢位。有了適當的尾呼叫,使用者可以進行的移動次數沒有限制,因為每個移動實際上會執行到另一個函數的 goto,而不是傳統的呼叫。

對於這個簡單的遊戲,您可能會發現資料驅動程式(您使用表格來描述房間和移動)是一個更好的設計。然而,如果遊戲在每個房間都有幾個特殊情況,那麼此狀態機設計非常合適。