Sven Olsen |
|
在那裡我發現了更好的東西,那就是 Peter Shook 精巧的 table unpack patch。自從那次發現後,調整 Lua 解析器的語法規則便成為我的小小樂趣。
比起用一大串用途可能見仁見智的小 patch 來佔用 power patch 網頁的空間,我決定試著將它們大部分記錄在這裡。同樣地,我仿效 PeterShook 的做法,因為他也似乎傾向將他一些比較有爭議性的語言調整說明移到他的個人 bio 網頁。
a=b (f or g)()
在 Lua 5.1 中,若給定以上程式碼,解析器會因「語法不明確」而發生錯誤。Lua 5.2 會接受此程式碼,將其解釋為單一陳述式,執行
a=b( f or g)()
一般而言,我比較喜歡 5.1 的行為。在升級到 Lua 5.2 後,我偶爾會寫出錯誤,而舊版的「語法不明確」檢查則會發現這些錯誤。
然而,如 Roberto 所指出,5.1 的「語法不明確」檢查存在一些問題。其一,它並未實際檢查語法是否不明確,而這項任務在單次通過的解析器中實務上是不可能的。相反地,此檢查僅在函式參數清單開始於新行時,就會產生錯誤來實現。因此,在 5.1 下
print ( "long string one", "long string two" )
會因為「語法不明確」而產生錯誤,儘管此程式碼顯然沒有任何不明確之處。
我調整了 Lua 解析器,使行為介於 Lua 5.1 與 5.2 之間。我的檢查運作方式是在 5.1 中所使用的條件中加入第二個條件,將錯誤限制為包含 2+ 個函式呼叫的表達式。我也更改了錯誤訊息文字,希望能更清楚地說明應將錯誤解釋為警告,以防使用危險的格式。
我修改過的檢查並不完美,就像 Lua 5.1 一樣,偶爾仍會對僅有一種可能解釋方式的程式碼產生錯誤。例如,以下會觸發錯誤,即使解析程式碼的方法只有一個
new_object (f or g)(state)
Lua 語法的優點在於,這些會造成麻煩的語法歧義實際上很少會出現。即使採用非常激進的 5.1 風格換行處理,程式設計師也鮮少會看到「語法不明確」錯誤。在我謹慎的檢查下,這種情況就更加罕見。
儘管情況可能很少見,但我認為若完全不處理這個問題,就是一個錯誤。
在所有我編寫的補丁中,這是唯一一個我嚴肅推薦並加入 Lua 官方分支的。它能防止 bug,而且代價極小。
這可能是你會發現最簡單的強化補丁。這是 Brian Palmer 在他的簡潔匿名函數補丁中新增的一行,主要是用來移除在「if」陳述句之後緊接「then」token 的需求。我擴充這個功能,並讓「for」陳述句之後也可以選擇性地使用「do」token。使用這個補丁會為程式語言加入一些潛在的解析怪異狀況。例如,如果你有一個像這樣的陳述句
if a then (f or g)() end
並移除「then」,最後會產生一段只會執行以下這行的程式碼
a( f or g )()
但正如以上的討論,對 Lua 程式設計師而言,模稜兩可的語法是有必要提防的,因為這是讓分號可選擇性的必要後果。在這裡發布的版本中,我把它與我的換行處理補丁一起封裝,因為與 5.2 的寬鬆解析規則一起使用它似乎有過度危險的疑慮。
這是受 Unix 命令列啟發的一段簡單語法糖,可以將
print | a+b ==> print(a+b)
運算子「|」具有最低的優先權。
一個相關的轉換允許在函數的最後一個引數中使用「|」。例如
f(x,y,|) { <complex table definition> } ==> f(x,y, { <complex table definition> ) }
關於要如何以簡寫方式簡化慣用語句「for ... in pairs(...) do」在 lua-l 上已經有許多討論。
我偏好一種語法糖,可以簡單轉換
for k,v ; t ==> for k,v in pairs*(t) }
這裡的「pairs」呼叫有一個星號,因為它是使用全域表格中的 pairs 版本來計算,而不是使用 _ENV.pairs,這在其他情況下會是預期的情況。因此,即使你將 _ENV 替換為其他東西,這種簡寫形式的行為也會或多或少符合預期。
如果你很樂意試用我的任何其他補丁,你可以將它們全部下載為一個 [超級補丁],這個補丁是基於 5.2.2。儘管我的大多數模組都是相當獨立的,但在它們之間有足夠的重疊性,可能會讓維護獨立的補丁檔案變得麻煩。Peter 的「表格解包」補丁與「複合賦值」和「所需欄位」語義重疊。「所需欄位」同時與「安全導覽」補丁共用一個 VM 變更。兩個「字串化」補丁都很小且獨立,但是如果你想取得它們的乾淨版本,你可以到 lua-l 的檔案中找到我對 diff 的演練 [1]。
正如我上面提到的,這是我的最愛強化補丁。而且如果你打算試試我任何一個語法模組,你肯定也應該試試 Peter 的。這個語法轉換
a,b,c in t ==> a,b,c=t.a,t.b,t.c.
這是一個奇妙的轉換,而這也由於 5.2 中新的 _ENV 規則而變得更有用。例如,如果你計畫將 _ENV 轉換為不尋常的東西,但想要讓某些標準全域函數保持作用範圍,你可以簡單地寫出
local pairs, ipairs, tostring, print in _ENV
不過,你也可以嘗試更多的微妙慣用語,其中大部分來自於將語法與元方法結合使用。考慮
local x,y,z,vx,vy,vz in INIT(0)
如果 INIT(a) 傳回一個具有「_index = function () return a end」的表格,則上述將會初始化所有給定的變數為 0。
類似的 __index 技巧將讓你顯著簡化大多數 require 語句樣板。例如,你可以定義一個 REQUIRE 代理物件,讓你替換
local socket = require 'socket' local lxp = require 'lxp' local ml = require 'ml'
為:
local socket, lxp, ml in REQUIRE
Peter 的語法如此強大是因為它提供給程式設計師一個將變數名稱轉換成字串的工具。不過,它僅在變數指派的情境中執行。一個更通用的工具將變數轉換成其匹配字串表示形式將會很有幫助;而雖然我想到的修補程式並不是和 Peter 的一樣簡潔或清楚,但我還是很常使用它。
修補程式套用兩個轉換。首先,在表格建構子的情境中,寫出
t = {..star, ..planet, ..galaxy} ==> t = {star=star, planet=planet, galaxy=galaxy}
類似地,在函式引數清單的情境中,寫出
f(..star,..planet,..galaxy) ==> f('star',star,'planet',planet,'galaxy',galaxy)
由於實作的奇想,在一個複雜運算式上使用「..」將會傳回在剖析那個運算式時遇到的最後一個字串、名稱或數字常數。因此
{..planet.star, ..planet, ..moon 'luna' } ==> { star=planet.star, planet=planet, luna = moon 'luna' }
這個方便的語意名稱取自 Groovy;而 CoffeeScript? 也有類似的功能。其想法是讓程式有能力檢查值而不觸發「嘗試索引 nil」錯誤。
例如,以下運算式會對物件圖示的亮光顏色求值,如果定義了亮光顏色,否則為白色
color = object?.icon?.glow?.color or white
未能定義物件,或是物件。圖示,或是物件。圖示。亮光,會導致運算式的第一部分對 nil 求值。
當我在 lua-l [2] 中提出此修補程式時,引起了不少人對此的熱情。不過,在語意如何運作的具體細節上也有一些分歧。
我自己的偏好是將「?」定義為一段相對簡單的語法糖,雖然它依賴於新增一個新的變數到全域名稱空間中。因此,當初始化一個新的 lua 狀態時,我會新增一個名為 _SAFE 的使用者資料到全域表格中,在那裡_SAFE 有 __index、__newindex、和 __call 全部設定為空程式、__len 設定為總是傳回 0,而 __pairs 和 __ipairs 定義為它們自己會傳回空程式的函式。
一旦我們有了這樣的使用者資料,新增一些語法糖就會相當簡單,它可以轉換
(<expr>?) ==> (<expr> or _SAFE*)
給定 _SAFE 的預設定義,這將導致的索引操作能依預期運作。但這語意也開啟了一些不錯的新功能。例如,如果你也在使用 Peter 的表格 unpack 修補程式,你可以寫出
local update, display in object?
結果就是如果未定義物件,則 update 和 display 將會是 nil。
類似地,你可以呼叫
update?()
結果是,僅當 update 已定義,才會執行。或者,你可以撰寫下列程式碼,它會反覆處理物件的所有圖示,前提是它們已定義。
for k,v in pairs(object.icons?)
但是,對於這個其他還算優雅的定義,有一個需要注意的地方。為了讓修補程式按預期運作,必須將從語法糖參照的「_SAFE」版本視為若 upvalue _ENV 等於 _G,則已評估的版本——否則,看似無害的一行程式碼(例如 _ENV = {}),會改變這個縮寫的意義。(這就是我在轉換定義中包含星號的原因。)
因此,儘管實作這個修補程式只需要小幅變更剖析器,還需要將 op 代碼 OP_GETGLOBAL 重新引入到 VM 中。
可能還值得指出的是,語意確實有點古怪,一個與「or」運算的解釋方式相關。具體來說
v = (nil)?.v ==> v=nil v = (false)?.v ==> v=nil v = (true)?.v ==> runtime error: attempt to index a boolean value
好幾位 lua-l 使用者認為此行為有點不太直觀;不過,我個人比較喜歡在 (false)?.v. 上擲回錯誤。
在撰寫安全導覽修補程式幾個月後——在 lua-l 上有一段很長的討論,討論了讓未定義的表格參照回傳 nil 可能導致錯誤的各種方式 [3]。例如,如果你正在撰寫建立所有物件名稱清單的程式碼,沿途反覆處理並設定
name[i] = object.name
如果你剛好遇到沒有名稱的物件,可能會導致後續出現奇怪的行為。
在某種程度上,這與促使寫出安全導覽修補程式的那個情況正好相反。「?」的目的,是讓 Lua 在原本會擲回錯誤的情況下回傳 nil。但是,肯定有某些情況需要相反的行為;比起回傳 nil,我們希望 Lua 擲回錯誤。因此,我已經寫好一個針對「必要欄位」運算子的修補程式。從表面上來看,它與我的安全導覽修補程式很相似。在最簡單的形式中,它會轉換
(object!) ==> (object or _MISSING*( "object" ) )
這裡,_MISSING 是透過 OP_GETGLOBAL 取得的全球變數,而 _GETGLOBAL 是安全導覽修補程式的一部份。因此,像這類的一行程式碼
name = get_name(object!,field!)
結果是
error: missing required value "object", if object==nil or error: missing required value "field", if field==nil
然而,我已經將事情推進得更進一步,擴充語法,讓包含「!」的表格查詢,如果回傳 nil 或 false,將會擲回錯誤。因此,一行像這樣的程式碼
age,height in record![name]!
可能產生的錯誤包括
error: missing required value "record", if record==nil error: <expr> is missing required field ["age"], if record[name].age==nil error: <expr> is missing required field [name -> "foo"], if name=='foo', and record.foo==nil
這並不是一個優雅的修補程式——在各個錯誤案例之間進行轉派會很混亂,而且產生的位元組碼效率不怎樣。即便如此,我一直覺得它很有用。它消除了我原本必須撰寫的大量樣板健全性檢查程式碼。
這是最初我去找的語法糖。我從 C 遺漏掉的所有好東西:+=、-=、*= 等。
在套件中包含的實作允許向量增量,例如
vx,vy,vz += ax, ay, az
你也可以使用開放函式呼叫,提供資料給清單中的任意額外值。例如:
px,py,pz += 1, calc_yz_vel()
但是,如果左右手邊數值之間的關係明顯有落差,剖析器會拋出一個錯誤訊息。
不同於標準的賦值,複合的賦值是由左至右評估。所以
local a,b=2,4 a,b+=b,a ==> a==6, b==10
另外,由於我對於這個 hack 的樂趣太多了,所以我也加上一個「++」syntax sugar。如果有多個左邊值,此值都會遞增 1。
i,j,k++ ==> i,j,k=i+1,j+1,k+1