Lua 正確的尾調用(proper tail call)


Lua支持“尾調用消除(tail-call elimination)”。
尾調用(tail call):當一個函數調用是另一個函數的最后一個動作時,該調用才算是一條“尾調用”。例如,下面的代碼就是一條“尾調用”:

function f (x) return g(x) end

也就是說,當f調用完g之后就再無其他事情可做了。因此在這種情況下,程序就不需要返回那個“尾調用”所在的函數了。所以在“尾調用”之后,程序也不需要保存任何關於該函數的棧(stack)信息了。當g返回時,執行控制權可以直接返回到調用f的那個點上。有一些語言實現(例如Lua解釋器)可以得益於這個特點,使得在進行“尾調用”時不好非任何棧空間。將這種實現稱為支持“尾調用消除”。
由於“尾調用”不會耗費棧空間,所以一個程序可以擁有無數嵌套的“尾調用”。例如,在第哦用以下函數時,傳入任何數字作為參數都不會造成棧溢出:

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

判斷當前的調用是一條“尾調用”的准則:一個函數在調用完另一個函數之后,是否就無其他事情需要做了。
例如,下面的代碼就不是一條“尾調用”:

function f (x) g(x) end

這個示例的問題在於,當調用完g后,f並不能立即返回,它還需要丟棄g返回的臨時結果。類似的,以下所有調用也都不符合上述准則:

return g(x) + 1        -- 必須做一次加法
return x or g(x)    -- 必須調整為一個返回值
return (g(x))        -- 必須調整為一個返回值

在Lua中,只有“return <func>(<args>)”這樣的調用形式才算是一條“尾調用”。Lua會在調用前對<func>及其參數求值,所以它們可以是任意復雜的表達式。舉例來說,下買的呢調用就是一條“尾調用”:

return x[i].foo(x[j] + a*b , i + j)


[“尾調用” 迷宮游戲示例]
“尾調用”類似一條goto語句。在Lua中“尾調用”的已答應永久是編寫“狀態機(state machine)”。這種程序通常以一個函數來表示一個狀態,改變狀態就是goto(或調用)到另一個特定的函數。舉一個簡單的迷宮游戲的例子來說明這個問題。例如,一個迷宮有幾間房間,每間房間中最多有東南西北4扇門。用戶在每一步移動中都需要輸入一個移動的方向。如果在某個方向上有門,那么用乎可以進入相應的房間;不然,程序就打印一條警告。游戲目標就是讓用戶從最初的房間走到最終的房間。
這個游戲就是一種典型的狀態機,其中當前房間就是一個狀態。可以將迷宮中的沒見房間實現為一個函數,並使用“尾調用”來實現從一件房間移動到另一間房間。在以下代碼中,實現一個具有4間房間的迷宮:

function room1 ()
    local move = io.read()
    if move == "sourth" 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 == "sourth" 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()

若沒有“尾調用消除”的話,每次用戶的移動都會創建一個新的棧層(stack level),移動若干步之后就有可能會導致棧溢出。而“尾調用消除”則對用戶已動的次數沒有限制。這是因為每次移動實際上都只是完成一條goto語句到另一個函數,而非傳統的函數調用。
對於這個簡單的游戲而言,或許會覺得將程序設計為數據驅動的會更好玩一點,其中將房間和移動記錄在一些table中。不過,如果游戲中的沒見房間都有個字特殊的情況的話,采用這種狀態機的設計會更為合適。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM