Sequence Adapters |
|
序列是迭代器函数生成的值的流。最简单的形式是,它们由一个函数(或可调用对象)表示,该函数每次被调用时都会返回一些值,并使用 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