Lua 範圍討論

lua-users home
wiki

這個頁面試圖用於常見的範圍語言來描述 Lua 範圍,但最後演變成範圍術語的辯論。如果要以非專家術語說明 Lua 範圍,請參考 LuaScoping

VersionNotice:在此頁面上的留言指涉 Lua 的舊版本(2001 年),它有更多受限的詞彙範圍且據稱不完全支援封閉。在近期版本中,這些留言許多都不適用。

Lua 採取靜態範圍 [1]

受限靜態範圍

從巢狀函式存取變數有問題。巢狀函式無法存取其週遭範圍中除了全域變數之外的變數。有提供特殊的「upvalue」語法,供以唯讀方式存取變數的拷貝(在匿名函式實體化時),但只能從定義巢狀函式的直接範圍中存取。

對於這種情況是否應稱做受限/錯誤靜態/詞彙範圍,有些分歧。一些電腦語言文字將詞彙範圍和靜態範圍定義成相同的,讓事情變得複雜。當存取到的變數位於全域範圍時,Lua 的作用就像詞彙範圍。

do
    -- assume "a" is a global with value 5
    local foo = function() print(a) end
    a = 10
    foo() -- prints "10"
end

當使用 upvalues 存取下一個外部範圍時,Lua 也像詞彙範圍一樣作用,只要不會將變數重新繫結(從變數範圍或巢狀函式重新繫結)。

do
    local a = 5
    local foo = function() print(%a) end
    foo() -- prints "5"
end

無論如何,與 Perl 等其他腳本語言年輕時相比,Lua 或許並不如它們那麼糟。Perl 從動態範圍做起,已慢慢發展成透過 my 限定條件來支援詞彙範圍。詞彙範圍可能會在 Perl 6 中成為預設值。

Python 以前有和 Lua 類似的受限靜態範圍。詞彙範圍會在 2.2 版中成為預設值,而且在 2.1 版中已成為一個選項 [2]。Python 實作的詞彙範圍不允許從巢狀函式中重新繫結變數,這類似於 Lua 的 upvalues。

對於 Lua,可能只需要將其受限子組擴充為真正的詞彙範圍即可。它可以延續 upvalue 的概念,而不允許重新繫結,就像 Python 一樣。或者也可以允許重新繫結,並消除 upvalues 的需求,而 upvalues 常常是 Lua 程式設計師混淆的來源。在 Python 詞彙範圍的設計文件中,引言不允許重新繫結的主要原因是 Python 沒有變數宣告。由於 Lua 需要 local 來限定變數,因此這不會是個問題。

備註

順帶一提,Lua 始終採行詞彙範圍。沒有例外,也沒有動態範圍規則。編譯器會決定採用哪個名稱宣告,如果它處理不了某個案例(外層函式的區域變數),便會發出錯誤。因此,問題不在於 Lua 有時是否採行詞彙範圍,而是在於它是否始終採行。Lua 只有一些許可事項的限制。(詞彙/動態範圍定義的是名稱解析的方式,而不是許可事項。)

-- E. Toernig

你必須找約翰·蘭斯戴爾陳述那個論點,我與他保持距離 :) 我並不否認 Lua 採靜態範圍。混淆的原因在於有些人區分「靜態範圍」和「詞彙範圍」。--JohnBelmonte

我感到困惑。我想我了解「靜態」和「動態」範圍的差別。「靜態」和「詞彙」的區別是什麼?--JamesHearn

我想混淆來自於公用程式在詞彙和靜態範圍術語上有兩種不兼容的定義。函數程式設計社群過去使用這些術語來表示包含巢狀範圍需求的意思,然而,龍書定義清楚暗示 Lua 同時採行詞彙和靜態範圍。--約翰·D·蘭斯戴爾

來自「編譯器,原理、技術,與工具」,Aho、Sethi,與 Ullman,1986 年,艾迪生魏斯理

一種語言的範圍規則決定對非區域性名稱的引用的處理方式。一個稱為詞彙靜態範圍規則的常見規則,根據程式文字本身,來決定適用於名稱的宣告。Pascal、C 和 Ada 是許多使用詞彙範圍的語言之一,其中附帶一條「最緊密巢狀」規範,後續將予說明。另一條稱為動態範圍規則的備用規則,根據目前的啟動狀態,在執行時期決定適用於名稱的宣告。Lisp、APL 和 Snobol 是使用動態範圍的語言之一。

將 FOLDOC 條目與龍書中的內容進行比較,後者的定義看起來稍微寬鬆,因為沒有提出「最小的區塊」規範是必要的,而且只提到那是某些語言(例如 C)的性質。換句話說,一個給定的範圍規則不需要是動態的(也就是說,不需要依賴目前的啟動狀態)才被視為靜態就夠了。

我對於諸如「受限制靜態範圍」和「真正的詞彙範圍」這些術語的意義感到疑惑。語言有,或者沒有,採行靜態範圍或詞彙範圍。在這些術語前面加上限定詞沒有意義。--約翰·D·蘭斯戴爾

Lua 目前缺乏靜態巢狀範圍

在 Lua 語言中,每個變數都具有區域或全域的範圍。可以在函數內定義函數,但巢狀函數無法取用其任何包函函數中定義的變數。因此,Lua 變數缺乏靜態巢狀範圍。造成此一結果,下列 Lua 程式碼是不合法的

function addn(x)
  function sum(y)
    return x+y
  end
  return sum
end
print((addn(3))(1))

此範例會不合法,因為在 addn 中定義的變數 x 不可以被巢狀函數 sum 取用。

巢狀函數可以取用其最直接包函函數中定義的變數的副本。在函數中取用變數的 upvalue 參考會在評估函數以產生閉包時提取此副本。一個變數在 percent 符號之前帶有一 percent 符號表示一個 upvalue 參考。在 sum 中 x 參考之前加上一個百分比符號,讓上述範例成為合法的 Lua 程式碼。

這項用於替代靜態巢狀範圍的糟糕做法已被採用,這是因為它很容易產出一個所有非全域變數都已堆疊分配的程式,但需要放棄靜態巢狀範圍才能有堆疊分配的非全域變數。

在 Python 2.2 版本以前的 Python 語言 دارای與 Lua 現有規則類似的範圍規則,每個變數都具有區域或全域的範圍。從 2.2 版本開始,Python 具有靜態巢狀範圍。在 Python 的程式碼實作中,從巢狀函數中參考的非全域變數是不變的。在此額外假設下,堆疊分配的非全域程式碼很容易被實作。

Python 的程式碼實作驗證了此方法的有效性。根據 Luca Cardelli 在 1984 年一篇名為「編譯函數語言」的論文中所述的內容,他們使用了平面式閉包程式碼,因而具備快速實作[3]。相關部分是關於「取用變數」的第 4 部分。

對於我們這些希望未來版本的 Lua 能夠具有具有靜態巢狀範圍的變數的人,我建議我們可以的幫助方法是尋找 Lua 核心實作人員可以使用的方法來進行程式碼實作。我認為我們應仔細研究 Python 2.2 實作中相關部分的點子,並將其提供給他們。

我自己不是 Python 程式設計師,但這些註解讓我能一窺 Python 的範圍規則。如果您發現有什麼錯誤,請糾正我。我認為 Python 2.0 與 Lua 有著巢狀函式中完全相同的問題。無法存取外部區域變數,而且沒有提供區域遞迴函式。用來將變數傳遞至區域函式的函式使用命名參數型式名稱=名稱。第一個名稱是函式的正式參數名稱,第二個名稱表示在函式宣告時凍結的外層範圍中的數值。因此,這與 Lua 中的上層值具有相同的行為。只是語法不同:在 Python 中,您在參數清單中置入 foo=foo 才能存取外部 foo ,而在 Lua 中您在函式內寫入 %foo 才能存取外部變數。語意相同。尤其是,您無法變更外部變數的值,而且對外部區域變數稍後的變更,也不會在已宣告的函式中反映出來。Lua 有個奇怪的限制(可以輕鬆移除且不會產生後相容性的問題),就是您只能存取直接封裝的範圍。我猜測 Python 有著相同的問題——您必須將值從函式傳遞到函式。

在 Python 2.2 中,他們變更範圍規則,讓編譯器自動產生等同於名稱=名稱參數的東西。您仍然無法變更外部名稱的值。但不知為何,他們現在可以遞迴呼叫區域函式。(兩個區域函式互相呼叫如何呢?這樣有辦法嗎?)或許是因為在區域函式後宣告的區域變數可以被函式看見,所以有些奇怪吧。但這依然不是如同 Pascal 或 Scheme 等語言中,大家熟知的詞彙範圍。

我在 lua-l 上發表的「可寫入上層值」標題修補程式(適用於 Lua 3.2),實作了 Python 2.2 的外層區域語意。唯一未實作的魔法是遞迴函式呼叫。但這似乎不是大部分人(包括我)希望看到的結果——主要是因為遞迴函式呼叫的問題。

-- ET

我相信用一種優秀的非常高級的程式語言撰寫的程式,對於不太熟悉該語言的人而言,是很容易就能理解的。除了上層值之外,我想 Lua 在這方面做得很好,因為它採用了類 Pascal 的語法用於控制結構,其意義正是大家所預期的。使用上層值的程式,不太可能讓一般使用者理解。人們需要閱讀手冊才能理解它們在哪裡取得值。大部分人直覺上都能理解靜態巢狀範圍。在從巢狀函式內參照變數時,讓變數變成不可變更,會為 Lua 程式的作者帶來負擔,但對程式的閱讀者而言則不然。我認為 Lua 的設計應優先滿足閱讀者的需求。

以下為靜態巢狀範圍的定義

在靜態嵌套範圍語言中,識別器的範圍固定在編譯時間以作為包含識別器宣告的最小的區塊 (起始/結束或函數/程序主體)。這表示在一些區塊中宣告的識別器僅能於該區塊中存取,及從在其中宣告的程序存取。

-- 約翰·D·拉姆斯戴爾

參考文件

摘錄自如何設計程式(以 Scheme 為基礎的教學文本)

另請參閱


近期變更 · 偏好設定
編輯 · 歷程
最終編輯時間為 2008 年 3 月 29 日晚上 11:49 GMT (diff)