Sven Olsen

lua-users home
wiki

我从 2010 年开始接触 Lua,大约是在 5.2 版本发布前一年。我很快就被这门语言吸引,尽管其中一些习惯用法让我觉得过于冗长。因此,不久之后,我就在 power patch 页面上寻找 += 的实现。

在那里我发现了一些更好的东西——Peter Shook 的巧妙的 table unpack patch。自从发现它之后,调整我的 Lua 解析器的语法规则就成了我的一个“罪恶的快乐”。

与其在 power patch 页面上列出一长串小的、可能有用的小补丁,我决定在这里记录其中大部分。同样,我遵循了 PeterShook 的做法;因为他似乎也决定将一些更有争议的语言调整文档移到他的个人简介页面上。

换行处理

  a=b
  (f or g)()

在 Lua 5.1 中,解析器在遇到上述代码时会抛出错误,抱怨“语法歧义”。Lua 5.2 会接受这段代码,将这两行解释为单个语句,执行

  a=b( f or g)()

总的来说,我更喜欢 5.1 的行为。升级到 Lua 5.2 之后,我偶尔会发现自己写了一些旧的“语法歧义”检查会发现的错误。

然而,正如 Roberto 指出的那样,5.1 的“语法歧义”检查存在问题。首先,它实际上并没有检查语法歧义——这在单遍解析器中几乎是不可能的。相反,该检查是通过在函数参数列表在新行开始时简单地抛出错误来实现的。因此,在 5.1 下

  print 
  (
   "long string one",
   "long string two"
  )

会导致“语法歧义”错误;尽管这段代码显然没有歧义。

我已经调整了自己的 Lua 解析器,使其行为介于 Lua 5.1 和 5.2 之间。我的检查通过在 5.1 中使用的检查基础上添加一个第二个条件来实现,将错误限制在包含 2 个以上函数调用的表达式的情况下。我还更改了错误消息的文本,希望使其更清楚地表明该错误应被解释为对危险格式的警告。

我修改后的检查并不完美——与 Lua 5.1 一样,它仍然会偶尔对只有唯一解释的代码抛出错误。例如,以下代码会触发错误,即使只有一个有效的解析方式

  new_object
  (f or g)(state)

Lua 语法最棒的一点是,这些类型的语法歧义在实际应用中很少出现。即使在非常激进的 5.1 风格换行处理下,程序员也很少会看到“语法歧义”错误。在我自己更谨慎的检查下,这种情况甚至更少见。

但虽然这可能是一个罕见的边缘情况,但我认为完全忽略这个问题是一个错误。

在我编写的所有补丁中,这是我唯一一个强烈推荐添加到官方 Lua 分支的补丁。它确实可以防止错误;而且它的成本很小。

[下载 5.2.2 版本]

可选的 "do" 和 "then" 标记

这可能是你见过的最简单的 powerpatch。它是一行代码,Brian Palmer 在他简洁的匿名函数补丁中包含了它——它消除了在“if”语句后面使用“then”标记的必要性。我将其扩展到类似地使在“for”语句后面使用“do”标记成为可选。使用该补丁将为语言添加一些潜在的解析怪癖。例如,如果你有一个这样的语句

  if a then
    (f or g)()
  end

并且你删除了“then”,你最终将生成只执行该行的代码

  a( f or g )()

但正如上面所讨论的——潜在的语法歧义是 Lua 程序员始终需要注意的——这是使分号可选的必要结果。在这个发布的版本中,我已经将它与我的换行处理补丁打包在一起;因为与 5.2 的宽松解析规则一起使用它似乎是不必要的危险。

[下载 5.2.2 版本]

管道

这是一个简单的语法糖,灵感来自 unix 命令行。它将

	print | a+b ==>  print(a+b)

运算符 '|' 具有最低的优先级。

一个相关的转换允许在函数的最后一个参数中使用 '|'。例如

	f(x,y,|) { <complex table definition> } ==> f(x,y, { <complex table definition> ) }

带 ';' 的 for 循环

在 lua-l 上,关于如何最好地编写一个可以简化惯用的 'for ... in pairs(...) do' 语句的简写,已经有很多争论。

我更喜欢的语法糖是简单地将

	for k,v ; t ==> for k,v in pairs*(t) }

这里的 'pairs' 调用带有一个星号,因为它使用全局表中的 pairs 版本进行评估,而不是 _ENV.pairs,否则将是这种情况。因此,即使你用奇怪的东西替换 _ENV,简写也会或多或少按预期工作。

更大的补丁

如果你好奇想尝试我的其他任何补丁,你可以将它们全部下载为一个 [超级补丁],基于 5.2.2。虽然我的大多数修改都是相当独立的——它们之间只是有足够的重叠,以至于维护独立的补丁文件很麻烦。Peter 的 Table Unpack 补丁与 Compound Assignment 和 Required Fields 语义都有重叠。同时,Required Fields 与 Safe Navigation 补丁共享一个 VM 更改。Stringification 补丁既小又独立——但如果你想要这两个补丁的干净版本,你可以在 lua-l 档案中找到我对差异的逐步说明 [1]

Table unpack

正如我上面提到的,这是我最喜欢的 powerpatch。如果你要尝试我的任何语法修改,你当然也应该尝试 Peter 的。语法将

	a,b,c in t  ==>  a,b,c=t.a,t.b,t.c.

这是一个很棒的转变,并且由于 5.2 中新的 _ENV 规则,它变得更加有用。例如,如果您计划将 _ENV 更改为一些不寻常的东西,但希望保留某些标准全局函数的作用域,您只需编写

  local pairs, ipairs, tostring, print in _ENV

但是,您也可以尝试更微妙的习惯用法,其中大多数来自将语法与元方法结合使用。考虑

 local x,y,z,vx,vy,vz in INIT(0)

如果 INIT(a) 返回一个带有“__index = function() return a end”的表,那么上面的代码将把所有给定的变量初始化为 0。

类似的 __index 技巧将让您显着简化大多数 require 语句的样板代码。例如,您可以定义一个 REQUIRE 代理对象,它允许您替换

  local socket = require 'socket'
  local lxp = require 'lxp'
  local ml = require 'ml'

 local socket, lxp, ml in REQUIRE

字符串化

Peter 的语法之所以强大,是因为它为程序员提供了一种将变量名转换为字符串的工具。但是,它只在变量赋值的上下文中这样做。一个更通用的工具,用于将变量转换为其匹配的字符串表示形式将很有用;虽然我提出的补丁不像 Peter 的那么优雅或清晰,但我确实经常使用它。

该补丁应用了两种转换。首先,在表构造函数的上下文中,编写

	t = {..star, ..planet, ..galaxy} ==>  t = {star=star, planet=planet, galaxy=galaxy}

类似地,在函数参数列表的上下文中,编写

	f(..star,..planet,..galaxy) ==> f('star',star,'planet',planet,'galaxy',galaxy)

由于实现上的一个怪癖,在复杂表达式上使用“..”将返回解析该表达式时遇到的最后一个字符串、名称或数字常量。因此

	{..planet.star, ..planet, ..moon 'luna' } ==> 
		{ star=planet.star, planet=planet, luna = moon 'luna' }

安全导航

这个方便语义的名称借鉴了 Groovy;通过 CoffeeScript? 也有类似的功能。这个想法是让检查值成为可能,而不会触发“尝试索引 nil”错误。

例如,以下表达式将评估为对象的图标的辉光颜色(如果定义了辉光颜色),否则为白色

  color = object?.icon?.glow?.color or white

未能定义 object、object.icon 或 object.icon.glow 将导致表达式的第一部分评估为 nil。

当我在 lua-l 上提出这个补丁时 [2],人们对此非常热情。但是,关于语义究竟应该如何工作的细节也存在一些分歧。

我个人更倾向于将“?”定义为一个相对简单的语法糖,尽管它依赖于向全局命名空间添加一个新变量。因此,当初始化一个新的 lua 状态时,我在全局表中添加了一个名为 _SAFE 的用户数据,其中 _SAFE 的 __index、__newindex 和 __call 都设置为 nullops,__len 设置为始终返回 0,__pairs 和 __ipairs 定义为自身返回 nullop 的函数。

一旦我们有了这样的用户数据,添加一些语法糖来转换就相当简单了

	(<expr>?) ==> (<expr> or _SAFE*)

鉴于 _SAFE 的默认定义,这会导致按预期工作的索引操作。但这种语义也开辟了一些新功能。例如,如果您也使用 Peter 的表解包补丁,您可以编写

  local update, display in object?

如果对象未定义,则更新和显示将为 nil。

类似地,你可以调用

  update?() 

这样,只有在定义了 update 时才会执行它。或者你可以编写以下代码,它将遍历所有对象的图标(如果已定义)。

  for k,v in pairs(object.icons?) 

但这个原本相当优雅的定义有一个需要注意的地方。为了使补丁按预期工作,从语法糖中引用的“_SAFE”版本需要像 upvalue _ENV 等于 _G 一样进行评估——否则,像 _ENV = {} 这样看似无害的行将改变简写的意思。(这就是我在转换定义中包含星号的原因。)

因此,虽然实现此补丁只需要对解析器进行少量更改,但也需要将 OP_GETGLOBAL 操作码重新引入 VM。

也许还值得指出,语义确实有一些怪癖,这与“或”运算的解释方式有关。具体来说

  v = (nil)?.v ==> v=nil
  v = (false)?.v ==> v=nil
  v = (true)?.v ==> runtime error: attempt to index a boolean value

几个 lua-l 用户认为这种行为有点不直观;虽然我个人更喜欢它,而不是在 (false)?.v 上抛出错误。

必填字段

在编写安全导航补丁几个月后——在 lua-l 上进行了一场关于未定义表引用返回 nil 会导致各种错误的讨论 [3]。例如,如果你正在编写代码来构建所有对象名称的列表,并进行迭代并设置

  name[i] = object.name

如果碰巧遇到一个没有名称的对象,可能会导致后续出现奇怪的行为。

从某种意义上说,这与激发安全导航补丁的场景正好相反。'?' 的目的是让 Lua 在原本会抛出错误时返回 nil。但是,当然也有一些情况需要相反的行为;我们希望 Lua 抛出错误,而不是返回 nil。因此,我编写了一个“必填字段”运算符的补丁。从表面上看,它与我的安全导航补丁非常相似。在最简单的形式中,它将

	(object!) ==> (object or _MISSING*( "object" ) )

这里 _MISSING 是一个全局变量,它通过 OP_GETGLOBAL 获取,如安全导航补丁所示。因此,像

	name = get_name(object!,field!)

将导致

	error: missing required value "object",  if object==nil or
	error: missing required value "field",  if field==nil

然而,我将事情推进了一步,增强了语法,以便包含“!”的表格查找在返回 nil 或 false 时会抛出错误。因此,对于像这样的行

	age,height in record![name]!

可能生成的错误包括

	error: missing required value "record", if record==nil
	error: <expr> is missing required field ["age"],  if record[name].age==nil
	error: <expr> is missing required field [name -> "foo"], if name=='foo', and record.foo==nil

这不是一个优雅的补丁——在各种错误情况之间进行调度会变得很混乱,生成的字节码效率也不高。即便如此,我发现它很有用。它消除了我原本需要编写的许多样板代码的必要性。

复合赋值

这是我最初寻找的语法糖。我从 C 中错过的所有好东西:+=、-=、*= 等。

捆绑包中包含的实现允许向量增量,例如

  vx,vy,vz += ax, ay, az

您还可以使用开放函数调用来为任意数量的附加值提供数据。例如

  px,py,pz += 1, calc_yz_vel()

但是,如果右手边和左手边的值数量明显不匹配,解析器将抛出错误。

与标准赋值不同,复合赋值是从左到右进行评估的。所以,

  local a,b=2,4
  a,b+=b,a
    ==> a==6, b==10 

此外——因为我对这个黑客玩得太过火了——我还添加了“++”语法糖。在多个左手边值的情况下,所有值都将加 1。

	i,j,k++    ==>    i,j,k=i+1,j+1,k+1

最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2015 年 3 月 23 日下午 8:15 GMT (差异)