Sequence Adapters

lua-users home
wiki

对序列操作更好的表示法有需求

序列是迭代器函数生成的值的流。最简单的形式是,它们由一个函数(或可调用对象)表示,该函数每次被调用时都会返回一些值,并使用 nil 表示结束。序列可以是单值的(如 io.lines)或多值的(如 pairs),并且大多数情况下它们会生成相同类型的值。将一些常用的序列和表操作提取出来,并作为库提供将会很有用。这是 PenlightLibraries 的主要目标之一。

(请注意,这是一个特有的定义:Lua 手册将序列定义为“一个表,其中所有正数键的集合等于某个整数 n 的 {1..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 元方法,然后该方法在 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 元方法无法查找函数名时,策略就是调用 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


RecentChanges · preferences
编辑 · 历史
最后编辑于 2012 年 7 月 4 日 上午 4:25 GMT (差异)