Lua 巨集 |
|
http://luaforge.net/frs/download.php/4329/luamacro-1.5.zip
具備 [tokenf 補丁程式] 的 Lua 5.1.4。
對好奇的人而言,這裡提供了一個使用 Mingw 進行修正的 Lua 5.1.4 Windows 版本(即,與 Windows 版 Lua 不相容)[按此]。
lhf 的 tokenf 補丁程式(另請參閱 [這篇寫作])提供了一個簡單但強大的鉤子,用來深入 Lua 編譯器看到的 Token 串流。(在 Lua 中,對於指定的模組而言,編譯為位元組碼與執行是分開的階段。)基本上,您必須提供一個稱為 FILTER 的全域函數,將用兩種截然不同的方式呼叫它。首先,它將包含兩個參數:一個讓您能夠取得下一個 Token(「取得程式」)及原始檔的函數。接著,它將呼叫時不包含參數,但預期傳回三個值。(這乍看之下會令人困惑,因此,這兩個函數可能應該給予不同的名稱。)
get
函數傳回三個值:行、Token 及值。Token 包含一些特殊值,例如 '<name>'
、'<string>'
、'<number>'
及 '<eof>'
,但其他部分則為實際的關鍵字或運算子,例如 'function'
、'+'
、'~='
、'...'
等。如果 Token 是特殊案例之一,則 Token 的值將作為第三個值傳回。(tokenf 配布版有一個指導範例,稱為 fdebug,它僅會印出這些值。)
Token 篩選程式會一次讀寫 Token。協同程式讓維護複雜的狀態成為可能,不必管理一個狀態機。
巨集此處說明的巨集功能類似於 C 預處理器,儘管它是針對一個已經預先消化過的 Token 串流運作,而且不是一個獨立的程式,Lua 程式必須通過它。這有幾個優點:速度更快(沒有獨立的轉譯階段),而且巨集能夠以互動方式進行測試。缺點是 LuaMacro 相依於一個修補過的 Lua 版本,而且進行巨集除錯有時會造成一些問題,因為您看不到成果以轉換文本方式呈現。
一如往常,巨集必須審慎使用。它不會共用 Lua 的範圍概觀(所以應明確命名),過度使用它們可能會造成只有原始撰寫者能夠閱讀其程式的結果,這就是所謂的「私有語言」問題。(請參閱 http://research.swtch.com/2008/02/bourne-shell-macros.html,取得一個經典範例。)
即使在生產/ 已發布的代碼中沒有宏,宏在除錯與建構測試時可能還是會很有用。如果你將 Lua 當作 DSL (Domain Specific Language),則宏可以輕鬆自訂語法。
此版本 (1.5) 允許 Thomas Lauer 所建議的簡化符號,其中簡單的宏看起來很像 C 的等價物
具有兩個參數的宏
macro.define('PLUS(L,C) ((L)+(C))')
下列項目等同於 C 式的斷言,其中實際表達式會轉換成字串,以使用「字串化」函式 _STR()
,形成 assert()
的選用參數第二個
macro.define('ASSERT('x') assert(x,_STR(x))')
這項功能的好處是,可以透過在標頭中做一個簡單變更,便能在全域移去斷言。
宏定義必須與待預處理的程式碼放在個別的檔案中,但不需要在程式執行前載入。此外還有一個標準宏 __include
。假設 PLUS
和 ASSERT
兩個宏已定義在 plus.lua
中,則
--test-macro.lua __include 'plus' print(PLUS(10,20)) ASSERT(2 > 4)
$ lua -lmacro test-macro.lua 30 lua: test-macro.lua:3: 2 > 4 stack traceback: [C]: in function 'assert' test-macro.lua:3: in main chunk
程式剖析之前載入模組 macro
很重要,因為宏是在編譯階段運作。
可以互動式測試這些宏,如下所示
D:\stuff\lua\tokenf>lua -lmacro -i Lua 5.1.2 Copyright (C) 1994-2007 Lua.org, PUC-Rio > __include 'plus' > = PLUS(10,20) 30 > = PLUS(10) =stdin:1: PLUS expects 2 parameters, received 1 > ASSERT(2 > 4) stdin:1: 2 > 4 stack traceback: [C]: in function 'assert' stdin:1: in main chunk [C]: ?
替換可能是函式--這正是事情變得有趣的地方
macro.define('__FILE__',nil,function(ls) return macro.string(ls.source) end)
nil
第二個參數表示我們沒有參數,而第三個替換參數是永遠會收到一個包含詞法狀態的表格函式:來源、行與 get(目前正在使用的取得函式)。預期這個函式會傳回一個權杖清單:在本例中,是 {'<string>',ls.source}
。有三個方便使用的函式,macro.string()
、macro.number()
和 macro.name()
。在 LuaMacro 1.5 中,此函式也可以傳回字串。
通常,替換函式會收到傳遞到宏的所有參數
local value_of = macro.value_of macro.define('_CAT',{'x','y'},function(ls,x,y) return macro.name(value_of(x)..value_of(y)) end)
這也是處理變動長度參數清單的唯一方法,因為不然的話,形式參數與實際參數的數量必須一致。請記住參數永遠會以權杖清單的形式出現,而這類清單會有特定的縮寫格式。例如, {'<name>','A','+',false,'<name>','B','*',false,'<number>',2.3}
。(這裡的 false
是 nil
的佔位符。)
請注意宏定義是 Lua 模組,因此你可以自由定義局部變數與函式。
新的簡化宏定義 (以字串表示) 允許在實際使用它們的來源檔案中,定義簡單的宏。這甚至適用於互動式解釋器
$ lua -lmacro
Lua 5.1.4 Copyright (C) 1994-2008 Lua.org, PUC-Rio
> __def 'dump(x) print(_STR(x).." = "..tostring(x))'
> x = 10
> dump(x)
x = 10
> dump(10*4.2)
10 * 4.2 = 42
(這可能是除錯工具箱中一個有用的工具。)
思考這個簡寫,目的是在陣列中的所有值都根據敘述進行評估
__def 'for_(t,expr) for _idx,_ in ipairs(t) do expr end' for_({10,20,30},print(_))
參考 functional.lua
,取得更多這個樣式的範例。
使用匿名函式時,常見的範例為
set_handler(function() ... end)
如果可以簡單地如下表達,那會很酷 (像LeafStorm的BeginEndProposal)
set_handler begin
...
end
有了LuaMacro 1.5,巨集可以設定詞法分析器,用以監控特定代幣的代幣串流。特別有用的會是一個「結束分析器」。在這種情況下,分析器會偵測到區塊最後一個end
,並傳出end)
。
def ('begin',nil,function() macro.set_end_scanner 'end)' return '(function(...)' end)
另一個LuaMacro 1.5 特色,是任何代幣都可以用作巨集名稱。考量引入一個簡短的匿名函式形式這個問題 (請參閱https://lua-users.dev.org.tw/lists/lua-l/2009-12/msg00140.html)。我們可以用\x(x+1)
取代function(x) return x+1 end
。許多讀者 (但不是全部:) ) 發現這個表示法在指定短函式時,較不吵雜。
「\」作為代幣巨集是不錯的選擇,因為它不會出現在程式語言的其他地方。您可以定義一個處理函式,在沒有參數清單的情況下,提供參數,供巨集呼叫。這是define()
的第四個參數。
-- lhf-style lambda notation def ('\\', {'args','body';handle_parms = true}, 'function(args) return body end', function(ls) -- grab the lambda -- these guys return _arrays_ of token-lists. We use '' as the delim -- so commas don't split the results local args = macro.grab_parameters('(','')[1] local body = macro.grab_parameters(')','')[1] return args,body end )
參數超過一個的函式 (例如\x,y(x+y
) 和定義函式的函式 (例如\x(\y(x+y))
) 會依預期運作。
作為實際的範例,以下是我們如何將try
和except
定義為pcall()
的語法糖
-- try.lua function pack (...) return {n=select('#',...),...} end macro.define ('try',nil, 'do local res = pack(pcall(function()' -- try block goes here ) macro.define ('except',{'e',handle_parms=macro.grab_token}, function() -- make sure that the 'end' after 'except' becomes 'end end' to close -- the extra 'do' in 'try'. -- we start at level 1 (before 'end))') and must ignore the first level zero. macro.set_end_scanner ('end end',1,true) return [[ end)) if res[1] then if res.n > 1 then return unpack(res,2,res.n) end else local e = res[2] ]] -- except block goes here end ) return 'local pack,pcall,unpack = pack,pcall,unpack'
因此,在以下程式碼中
a = nil try print(a.x) except e print('exception:',e) end
編譯器會看到以下程式碼
a = nil do local res = pack(pcall(function() print(a.x) end)) if res[1] then if res.n > 1 then return unpack(res,2,res.n) end else local e = res[2] print('exception',e) end end
這些巨集的智慧 (注意我們可以處理結束多餘的do
陳述式) 表示,在不修補 Lua 本身的情況下,只要付出一些工作,就能更容易試用新的語法建議。而且用 Lua 編寫巨集比用 C 編寫語法擴充指令容易太多了!
(請注意,這並不是此問題的完整解決方案。特別是,我們無法處理顯式傳回,但沒有傳回值的區塊。)
最後的return
陳述式需要一些說明。這個巨集預設擴充的環境可以存取函式pack
、pcall
和unpack
。一般來說,這並不正確,因為使用module(...)
建立的模組預設無法存取全域環境。
這個巨集應該在module
呼叫之前,使用__include try
納入模組。__include
內部會使用require
,如果這傳回字串,那就是__include
巨集擴充的實際替代值。如此一來,巨集必要的隱藏相依性就會適當地出現在模組中。
作為較精細的程式碼產生範例,以下是一個類似於 C++ 語法的 using
巨集。Lua 沒有真正的模組範圍,因此常見的技巧是「展開」一個表格
local sin = math.sin local cos = math.cos ...
我們不僅得到了好用的未限定名稱,而且存取本機函數參照也比在表格中查詢函數來的快。以下是能自動產生以上程式碼的巨集
macro.define('using',{'tbl'}, function(ls,n) local tbl = _G[n[2]] local subst,put = macro.subst_putter() for k,v in pairs(tbl) do put(macro.replace({'f','T'},{macro.name(k),n}, ' local f = T.f; ')) end return subst end)
此處替換是一個函數,會傳遞一個名稱指示(例如 {'<name>','math'}),假設其參考一個全域可用的表格,然後動態反覆運算該表格以產生所需的 local
指派。subst_putter()
會提供一個指示清單和一個 put
函數;你可以使用 put
函數填充指示清單,該清單隨後會傳回並實際替換到指示串流中。replace
會產生一個新的指示清單,方法是將指示清單中的所有形式參數(第一個引數)取代為實際參數值(第二個引數)。要在模組開頭使用此巨集呼叫
using (math)
這會將表格的全部內容納入範圍,並假設表格確實存在於 編譯時間。更好的寫法是 import(math,sin cos)
,它會擴充為 local sin = math.sin; local cos = math.cos
macro.define ('import',{'tbl','names'}, function (ls,tbl,names) local subst,put = macro.subst_putter() for i = 1,macro.length_of(names) do local name = macro.get_token(names,i) put 'local'; put (name); put '='; put (tbl); put '.'; put (name); put ';' end return subst end )
在 PythonLists 中,FabienFleutot 討論了建模在 Python 語法的 list 概括運算語法。
x = {i for i = 1,5}
{1,2,3,4,5}
此類陳述實際上不需要太多轉換即可成為有效的 Lua。我們使用匿名函數
x = (function() local ls={}; for i = 1,5 do ls[#ls+1] = i end; return ls end)()
然而,為了讓它作為一個巨集運行,我們需要選擇一個名稱(此處為 'L'),因為我們無法預先看到 `for` 指示。
macro.define('L',{'expr','loop_part',handle_parms=true}, ' ((function() local t = {}; for loop_part do t[#t+1] = expr end; return t end)()) ', function(ls) local get = ls.getter local line,t = get() if t ~= '{' then macro.error("syntax: L{<expr> for <loop-part>}") end local expr = macro.grab_parameters('for') local loop_part = macro.grab_parameters('}','') return expr,loop_part end)
替換相當直接,但我們必須使用自訂函數來擷取參數。第一次呼叫 macro.grab_parameters
會擷取到 'for' 為止,第二次會擷取到 '}' 為止。在此,我們必須小心不要將逗號視為本擷取的區隔符號,方法是將第二個引數設定為空字串。
可以使用任何有效的 for 迴圈部分
L{{k,v} for k,v in pairs{one=1,two=2}} { "one", 1 }, { "two", 2 } }
巢狀概括運算會依預期運作
x = L{L{i+j for j=1,3} for i=1,3} { { 2, 3, 4 }, { 3, 4, 5 }, { 4, 5, 6 } }
一種特別酷的寫法是將整個標準輸入作為一個 list 擷取,僅需一行
lines = L{line for line in io.lines()}
有個變數 macro.verbose
,你可以設定它以查看 LuaMacro 讀寫的指示。如果它是 0,會設定一個除錯掛鉤,但不會顯示除錯輸出;如果它是 1,之後它就會顯示編譯器看到的轉換過指示串流;如果它是 2,它還會顯示輸入指示串流。
將詳細程度設為 0(例如透過 lua -lmacro -e "macro.verbose=0" myfile.lua
)很有用,因為 __dbg
內建巨集之後就能動態變更詳細程度。
__dbg 1 mynewmacro(hello) __dbg 0
這個可以幫你精確找出特定問題,無需涉足多頁文件。
儘管 LuaMacro 仰賴經過補丁的代碼過濾器 Lua 編譯器,但產生的位元組碼可以在 stock Lua 5.1.4 上執行。一個非常簡單的編譯器已經提供,基於 Lua 發行的 luac.lua
。
$ lua macro/luac.lua myfile.lua myfile.luac $ lua51 myfile.luac <runs fine>
-- SteveDonovan