序列配接器 |
|
序列是由反覆運算函數產生的值順序串流。最簡單的形式,是由函數(或可呼叫物件)每次呼叫時傳回數個值,並以 nil
指出結束。序列可以是單值(像是 io.lines
)或多值(像是 pairs
),而且大多會產生相同類型的值。擷取一些在序列和表格上使用的通用運算會很有用,並將它們做成函式庫。這是 PenlightLibraries 主要的目標之一。
(請注意,這是一個主觀的定義:Lua 使用手冊定義 序列 為「一個表格,其中所有正整數鍵的集合等於 {1..n},且 n 為某個整數」[1]。)
使用 pl.seq 與 pl.func 提供的保留字元表達式,我們可以說
seq.printall(seq.filter(seq.list{1,2,3,4},Gt(_1,2)))
這還蠻不錯的,但很奇怪;我個人覺得這比一個簡單的迴圈難以激勵人心。當然,這只是一個玩具範例,但更複雜的組合會更難讓人解析。序列配接器可以將相同的運算表示為一個方法鏈
S{1,2,3,4}:filter(Gt(_1,2)):printall()
這比較容易閱讀,有幾個原因。首先,沒有到處都是 seq.
這個限定字,其次,我們從文化上習慣由左至右閱讀運算順序,這與由右至左的函數應用不同。(這也是 Unix 讓大家印象深刻的有名管線比喻。)以下是一個範例,其中長度運算元會套用在字串序列上
S{'one','tw','t'} :map '#' :printall() --> output: 3 2 1
使用真實序列會更有趣,例如由 io.lines
產生的。這會產生一個檔案中所有獨特列的序列,然後將它複製到一個表格中。
ls = S(io.lines(fname)):unique():copy()
另一個非常方便的樣式是將方法套用在序列的所有元素上
S{'[one]','[two]','[three]'}:sub(2,-2):upper():printall() --> output: ONE TWO THREE
以下是一個比較複雜的範例;假設有一個 Lua 程式碼字串,找出所有變數名稱。lexer.lua
會產生一個雙值序列,其中第一個值是代幣類型,第二個值是代幣值。第一個值用來過濾代幣串流,這樣就只有變數('iden')會通過;map(_2)
只允許值通過,unique
收集名稱,最後 copy
將這些集合成一個表格。
str = 'for i=1,10 do for j = 1,10 do print(i,j) end end' ls = S(lexer.lua(str)):filter(Eq(_1,'iden')):map(_2):unique():copy() print(List(ls)) ---> output: {i,print,j}
來源在 [這裡]。
因為沒有類型可以唯一識別序列(除了它可以呼叫之外),所以我們需要一個包裝物件。反覆運算函數會放入一個表格中,然後附加一個元表,這樣我們就可以控制方法查詢。
__index
這種 metamethod 會在每次呼叫到未知方法時,在 seq
表中查詢對應的方法。然而,這個函式無法直接使用,因為 (a) 它預期第一個參數是順序,(b) 它必須回傳它的結果給順序包裝程式,這樣我們才能繼續方法鏈。
SMT = { __index = function (tbl,key) local s = seq[key] if s then return function(sw,...) return S(s(sw.iter,...)) end end end, } function S (iter) return setmetatable({iter=iter},SMT) end
此方法示範了這個概念,但是實際上無法處理所有必要的狀況。有些 seq
函式會回傳單純值 (例如 reduce
) 或表格 (例如 copy
),包裝這些會造成混淆。像 map
之類的函式會以錯誤的順序排列參數 (先函式,後順序)。最好能讓 S
將表格包裝成順序,但不要對 copy
結果做同樣的動作。依此類推。而且還有個問題是方法如何套用於順序值上。關鍵在於 seq
中的這個函式
function mapmethod (iter,name,arg1,arg2) local val = iter() local fn = val and val[name] return function() if not val then return end local res = fn(val,arg1,arg2) val = iter() return res end end
與單純的 map
不同,mapmethod
會獲取函式 名稱,它會在順序產生的第一個值中找出名稱,之後回傳一個順序,也就是對每個值套用函式後的結果。(方便起見,前兩個額外的參數會明確擷取以利函式呼叫)。因此,__index
metamethod 在無法查到函式名稱時,會呼叫 mapmethod
並帶入方法的名稱。
-- seqa.lua local seq = require 'pl.seq' -- can't look these directly up in seq because of the wrong argument order... local overrides = { map = function(self,fun) return seq.map(fun,self) end, reduce = function(self,fun) return seq.reduce(fun,self) end } SMT = { __index = function (tbl,key) local s = overrides[key] or seq[key] if s then return function(sw,...) return SW(s(sw.iter,...)) end else return function(sw,...) return SW(seq.mapmethod(sw.iter,key,...)) end end end, __call = function (sw) return sw.iter() end, } function callable (v) return type(v) == 'function' or getmetatable(v) and getmetatable(v).__call end function S (iter) if not callable(iter) then if type(iter) == 'table' then iter = seq.list(iter) else return iter end end return setmetatable({iter=iter},SMT) end function SW (iter) if callable(iter) then return setmetatable({iter=iter},SMT) else return iter end end