扩展 For 和 Next |
|
Lua API 使得集成“外部”对象(包括外部映射)变得非常容易,但没有提供遍历它们的工具。也就是说,通过定义 gettable
和 settable
标签方法,可以将索引的外部对象完全集成到 Lua 环境中,但这不适用于使用 for k,v in foreign_map
或 k,v = next(foreign_map, k)
。这两种结构都很有用。
我还对将 next
扩展到“生成器”函数的可能性感到好奇。生成器函数的工作方式与 Lua next
库函数完全相同;给定一个迭代状态,它会生成下一个迭代状态和一个值。举个具体的例子,我可以将生成器定义为字符字符串的闭包,这样我就可以说
keywords = words "do else elseif end for if in repeat unless while" for i, v in keywords do dict[v] = (dict[v] or 0) + 1 end
这让我觉得相当优雅。
事实证明,该补丁非常简单,因此我已将其发布在文件区域,供那些想要尝试使用它的人使用。
该补丁提供了一个新的标签方法 "next"
,它由 for k,v in obj do end
结构调用;在 next
库函数中;以及在 lua_next
API 中。与 Lua 风格一致,提供了一个新的 rawnext
库函数和 lua_rawnext
API,它们避免使用标签方法。它们的工作方式与表相同,区别在于它们返回并使用“迭代状态”而不是“键”。如果对象还定义了 gettable
和 settable
标签方法,最好将键和迭代状态设置为相同的东西;但是,对于纯迭代对象,迭代状态可以是任何东西,只要 nil
表示迭代的开始和结束即可。实际上,它不需要在每次迭代中都不同。
此外,修补后的结构可以迭代函数,而不是表或带有“next”标签方法的对象;该函数使用单个参数调用,该参数是迭代状态,并且必须返回两个参数,第一个是下一个迭代状态,第二个是关联的值。在next
和lua_next
的情况下,这实际上只有美观价值,但它避免了必须知道被迭代者是函数还是对象。
上面描述的words
生成器定义如下
function words(str) return function(k) local _, k, v = strfind(%str, "([%w]+)", (k or 0) + 1) return k,v end end
如您所见,它保留其参数作为闭包,并使用每个单词中最后一个字符的索引作为迭代状态。
类似地,我可以定义
function wordsInFile(file) return function(k) k = read(%file, "*w") return k, k end end
在这种情况下,迭代状态在迭代期间基本上毫无意义;它仅用于表示终止。显然,可以实现更复杂的实现。
过滤器和转换器接受一个生成器并返回另一个生成器。一个简单的转换器示例是
function toupper(gen) return function(k) local v k, v = next(%gen, k) if k then return k, strupper(v) end end end
这可以推广
function gmap(fn, gen) return function(k) local v k, v = next(%gen, k) return k, k and %fn(v) end end
现在我可以写类似的东西
for i, v in toupper(words(str)) do -- something end
过滤器类似于转换器,但不是一对一的。一个示例过滤器类似于 Perl 的grep
函数
-- given a predicate and a generator, generate only those -- values for which the predicate returns true function gfilter(fn, gen) return function(k) local v k, v = next(%gen, k) while k do if %fn(v) then return k, v end k, v = next(%gen, k) end end end
我希望这能让你了解一些可能性。
大部分补丁都在lvm.c
中。在这里,我修改了操作码LFORPREP
和LFORLOOP
以使用新的 vm 函数luaV_next
,该函数实现了对 ttype 和标签方法的切换。next
和lua_next
也被修改为使用相同的函数,从而产生(我希望)一致的结果。
这确实会稍微减慢 for 循环的速度,因为测试是在循环的每次迭代中完成的。在运行 FreeBSD 3.2(我的开发机器)的 P3/866 上具有 128MB RAM 的计时测试表明,一个非常基本的 for 循环在打补丁后大约慢了 10%。 (另一方面,next 快了大约 4%。)我有一个稍微复杂的补丁,它在堆栈上缓存标签方法,导致速度仅下降 7%,但我还没有对它感到满意到可以发布它。我相信 4.1alpha 架构更适合这样做,我目前正在努力完成 4.1alpha VM 以尝试这样做。
对解析器或 for 循环期间的堆栈布局没有进行任何更改,因此修补后的 Lua 应该与二进制兼容;它只提供额外的功能。但是,我不会对这个说法发誓。
我认为所有这些都是一个有趣的实验。我认为与补丁一起提供的示例代码展示了新结构的表达能力,这与定义可迭代的用户数据的能力无关。
但是,迭代状态和键之间的混淆仍然困扰着我。我注意到在 4.1alpha 中,列表 for 实际上是使用迭代状态和键实现的,我认为这更合乎逻辑(并且应该稍微快一些);我对扩展 for 循环和next
库函数的语义以允许这样做感兴趣。虽然键通常可以用作迭代状态,但它并不总是方便(如words
示例所示)。
虽然在 for 循环中隐藏迭代状态很诱人,但这会降低灵活性。使用 for 循环和生成器而不是传统的 map
函数的主要优势在于,可以放弃迭代或修改迭代。也就是说,应该(至少)能够在 for 循环中使用 next
来跳过元素。在某些情况下,还可以通过为迭代状态变量赋值来重新启动迭代。
显然,生成器有很多类别,需要充分记录。它们可能是
同时,键值模型有点僵化。在向量的情况下,键可能对我来说并不那么有趣;在数据库的情况下,我可能希望使用返回多个值的生成器。理论上,这可以通过将 for 语法的扩展到类似 for name_list in exp1 [, exp1] ...
的形式来实现(第二个 exp1 将是一个起始迭代状态,默认值为 nil
)。
欢迎任何想法;您可以通过 rlake(at)oxfam.org.pe 联系我。或者直接在这里或在邮件列表中发布。
我建议您使用选项 "-urN" 创建补丁,并将测试程序包含在补丁中。--JohnBelmonte