序列配接器

lua-users home
wiki

改善序列運作符號的必要性

序列是由反覆運算函數產生的值順序串流。最簡單的形式,是由函數(或可呼叫物件)每次呼叫時傳回數個值,並以 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

SteveDonovan


最近變更 · 喜好設定
編輯 · 歷史記錄
最後編輯時間為 2012 年 7 月 4 日,上午 10:25 (格林威治標準時間) (diff)