9.3 用作迭代器的协同 |
我们可以将循环的迭代器看作生产者-消费者模式的特殊的例子。迭代函数产生值给循环体消费。所以可以使用协同来实现迭代器。协同的一个关键特征是它可以不断颠倒调用者与被调用者之间的关系,这样我们毫无顾虑的使用它实现一个迭代器,而不用保存迭代函数返回的状态。
我们来完成一个打印一个数组元素的所有的排列来阐明这种应用。直接写这样一个迭代函数来完成这个任务并不容易,但是写一个生成所有排列的递归函数并不难。思路是这样的:将数组中的每一个元素放到最后,依次递归生成所有剩余元素的排列。代码如下:
function permgen (a, n)
if n == 0 then
printResult(a)
else
for i=1,n do
-- put i-th element as the last one
a[n], a[i] = a[i], a[n]
-- generate all permutations of the other elements
permgen(a, n - 1)
-- restore i-th element
a[n], a[i] = a[i], a[n]
end
end
end
function printResult (a)
for i,v in ipairs(a) do
io.write(v, " ")
end
io.write("\n")
end
permgen ({1,2,3,4}, 4)
有了上面的生成器后,下面我们将这个例子修改一下使其转换成一个迭代函数:
1. 第一步printResult 改为 yield
function permgen (a, n)
if n == 0 then
coroutine.yield(a)
else
...
2. 第二步,我们定义一个迭代工厂,修改生成器在生成器内创建迭代函数,并使生成器运行在一个协同程序内。迭代函数负责请求协同产生下一个可能的排列。
function perm (a)
local n = table.getn(a)
local co = coroutine.create(function () permgen(a, n) end)
return function () -- iterator
local code, res = coroutine.resume(co)
return res
end
end
这样我们就可以使用for循环来打印出一个数组的所有排列情况了:
for p in perm{"a", "b", "c"} do
printResult(p)
end
--> b c a
--> c b a
--> c a b
--> a c b
--> b a c
--> a b c
perm函数使用了Lua中常用的模式:将一个对协同的resume的调用封装在一个函数内部,这种方式在Lua非常常见,所以Lua专门为此专门提供了一个函数coroutine.wrap。与create相同的是,wrap创建一个协同程序;不同的是wrap不返回协同本身,而是返回一个函数,当这个函数被调用时将resume协同。wrap中resume协同的时候不会返回错误代码作为第一个返回结果,一旦有错误发生,将抛出错误。我们可以使用wrap重写perm:
function perm (a)
local n = table.getn(a)
return coroutine.wrap(function () permgen(a, n) end)
end
一般情况下,coroutine.wrap比coroutine.create使用起来简单直观,前者更确切的提供了我们所需要的:一个可以resume协同的函数,然而缺少灵活性,没有办法知道wrap所创建的协同的状态,也没有办法检查错误的发生。