扩展 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,它们避免使用标记方法。它们的工作方式与表(tables)完全相同,只是它们返回并使用“迭代状态”而不是“键”。如果对象还定义了 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 来使用新的虚拟机函数 luaV_next,该函数实现了对 ttype 和标记方法的切换。next 和 lua_next 也被修改为使用相同的函数,产生(我希望)一致的结果。
这确实会稍微减慢 for 循环的速度,因为每次循环都会进行测试。在 P3/866(128MB RAM,运行 FreeBSD 3.2,这是我的开发机)上进行的计时测试表明,一个非常基本的 for 循环在打补丁后大约慢了 10%。(另一方面,next 的速度快了约 4%。)我有一个更复杂的补丁,它将标记方法缓存到堆栈中,从而将速度降低到仅 7%,但我还没有对此感到满意,暂时不发布它。我相信 4.1alpha 架构更适合这一点,我目前正在研究 4.1alpha 虚拟机,试图实现它。
解析器或 for 循环期间的堆栈布局没有改变,因此打补丁的 Lua 应该是二进制兼容的;它只提供了额外的功能。但是,我不敢保证这一点。
我认为所有这些都是一个有趣的实验。我认为补丁随附的示例代码展示了新构造的表达能力,这与定义可迭代的用户数据(userdata)的能力无关。
然而,迭代状态和键之间的混淆仍然让我困扰。我注意到在 4.1alpha 中,列表 for 实际上是使用迭代状态 *和* 键来实现的,我认为这更合乎逻辑(并且应该稍微快一点);我对扩展 for 循环和 next 库函数的语义以允许这一点很感兴趣。虽然键经常可以作为迭代状态使用,但并非总是方便(如 words 示例所示)。
虽然隐藏 for 循环中的迭代状态很诱人,但它降低了灵活性。使用 for 循环和生成器而不是传统的 map 函数的主要优点是能够放弃或修改迭代。也就是说,(至少)应该可以在 for 循环中使用 next 来跳过元素。在某些情况下,通过为迭代状态变量赋值来重新启动迭代也可能是可能的。
显然,生成器有许多类别,需要充分记录。它们可能是
同时,键-值模型有点僵化。对于向量(vector),键可能对我来说不是很有趣;对于数据库,我可能想使用一个返回多个值的生成器。理论上,这可以通过将 for 的语法扩展到类似 for name_list in exp1 [, exp1] ...(第二个 exp1 是一个带有 nil 作为默认值的起始迭代状态)来解决。
我欢迎任何想法;您可以通过 rlake(at)oxfam.org.pe 与我联系。或者直接在这里,或在邮件列表中发表。
我建议您使用选项“-urN”来创建补丁,并将测试程序包含在补丁中。--JohnBelmonte