扩展 For 和 Next

lua-users home
wiki


[!] 版本说明:以下代码适用于旧版本的 Lua,Lua 4。它在 Lua 5 下无法运行。

Lua 4.0 的实验性补丁

Lua API 使得集成“外部”对象(包括外部映射)变得非常容易,但没有提供遍历它们的工具。也就是说,通过定义 gettablesettable 标签方法,可以将索引的外部对象完全集成到 Lua 环境中,但这不适用于使用 for k,v in foreign_mapk,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,它们避免使用标签方法。它们的工作方式与表相同,区别在于它们返回并使用“迭代状态”而不是“键”。如果对象还定义了 gettablesettable 标签方法,最好将键和迭代状态设置为相同的东西;但是,对于纯迭代对象,迭代状态可以是任何东西,只要 nil 表示迭代的开始和结束即可。实际上,它不需要在每次迭代中都不同。

此外,修补后的结构可以迭代函数,而不是表或带有“next”标签方法的对象;该函数使用单个参数调用,该参数是迭代状态,并且必须返回两个参数,第一个是下一个迭代状态,第二个是关联的值。在nextlua_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中。在这里,我修改了操作码LFORPREPLFORLOOP以使用新的 vm 函数luaV_next,该函数实现了对 ttype 和标签方法的切换。nextlua_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


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2006 年 12 月 31 日凌晨 12:18 GMT (差异)