Lua SBLP 2001 邀请论文

转载自《巴西第五届编程语言研讨会论文集》(2001 年)B-14–B-28。[ps]

扩展语言的演变:Lua 的历史

作者:Roberto Ierusalimschy、Luiz Henrique de Figueiredo、Waldemar Celes

摘要。

自 1993 年诞生以来,Lua 编程语言已经远远超出了我们最乐观的预期。在本文中,我们将描述 Lua 的发展历程,从作为两个特定项目的内部语言创建,到 2000 年 11 月发布的 Lua 4.0。我们将讨论其一些概念的演变及其实现中的主要里程碑。

引言

有一个老笑话是这样说的:“骆驼是委员会设计的一匹马”。在编程语言领域,这个笑话几乎和关于委员会设计的编程语言的传说一样流行。这个传说得到了 Algol 68、PL/I 和 Ada 等语言的支持,这些语言都是由委员会设计的,但没有达到其赞助者的期望。

然而,除了委员会之外,对于这些语言部分失败还有另一种理论:它们都是生来就很大的。它们每一个都遵循自顶向下的设计过程,在任何程序员尝试它之前,甚至在任何编译器构建之前,该语言就已经完全指定。

另一方面,大多数成功的语言都是养育出来的,而不是设计的。它们遵循自底向上的过程,从一种小语言开始,通常目标很小。随着人们开始使用该语言,设计缺陷浮出水面,添加了新特性(或最终删除),有争议的要点得到了澄清(或最终变得模糊)。因此,语言的演变方式是编程语言中一个重要的研究课题。例如,SIGPLAN 已经赞助了两次编程语言历史会议 [31,3]

在本文中,我们将讲述 Lua 编程语言的历史。自诞生以来,作为两个特定项目的内部语言,Lua 已经远远超出了我们最乐观的预期。我们认为,这一成功的首要原因在于我们最初的设计决策:保持语言简单小巧;保持实现简单、小巧、快速、可移植且免费。

Lua 语言是由一个委员会设计(或者更确切地说,是培养)出来的;这个委员会相当小,只有三个成员,但它是一个委员会。事后看来,我们认为由一个小委员会培养对这门语言来说非常有利。只有当我们达成一致意见时,我们才会纳入一个新特性;否则,就留待将来。以后添加特性比删除特性容易得多。这种开发过程对保持语言的简单性至关重要,而简单性是我们最重要的资产。Lua 的大多数其他特性(速度、小巧和可移植性)都源于其简单性。

从其第一个版本开始,Lua 就有了“真正的”用户,即除了我们自己之外的其他用户。他们始终通过建议、投诉、使用报告和问题为这门语言做出重要贡献。同样,我们的小委员会发挥了重要作用。其结构给了我们足够的惯性,以便我们能够在不采纳所有建议的情况下听取用户意见。

我们按时间顺序组织了本文的其余部分。我们从 1993 年导致 Lua 创建的先前经验开始,然后经历了八年的讨论、决策、工作和乐趣。

开始

我们在 TeCGraf 使用内部设计的语言的第一次经验出现在一个数据输入应用程序中。巴西石油公司 (PETROBRAS) 的工程师每天需要多次为模拟器准备输入数据文件。这个过程既枯燥又容易出错,因为模拟程序是需要严格格式化输入文件的遗留代码——通常是数字的空白列,没有指示每个数字的含义。当然,每个数字都有一个特定的含义,工程师们在看到特定模拟的图表后,可以一目了然地理解。PETROBRAS 要求 TeCGraf 为这种数据输入创建多个图形前端。然后,只需单击图表中的相关部分,就可以交互式地输入数字——这比编辑数字列容易得多,而且更有意义。此外,它还提供了添加数据验证的机会,还可以从输入数据中计算出派生量,从而减少了用户所需的数据量,并提高了整个过程的可靠性。

为了简化在 TeCGraf 开发这些前端,我们决定以统一的方式对它们进行编码,因此我们设计了一种简单的声明式语言来描述每个数据输入任务 [12]。下面是这种语言(我们称之为 DEL(数据输入语言))中一个典型程序的部分内容

   :e      gasket            "gasket properties"
   mat     s                  # material
   m       f       0          # factor m
   y       f       0          # settlement stress
   t       i       1          # facing type

   :p
   gasket.m>30
   gasket.m<3000
   gasket.y>335.8
   gasket.y<2576.8
语句 :e 定义了一个实体(在示例中称为 gasket),它有一些具有默认值的字段。语句 :pgasket 的值定义了一些限制,从而实现了数据验证。DEL 还具有用于指定如何输入和输出数据的语句。

DEL 中的实体本质上是传统编程语言中的结构或记录。不同之处在于,它的名称也出现在图形元文件中,该元文件包含工程师在上面进行数据输入的关联图表,如上所述。

这种简单的语言被证明是成功的,无论是在 TeCGraf(因为它简化了开发),还是在用户中(因为它简化了定制数据输入应用程序)。很快,用户就开始要求 DEL 提供更多功能,例如用于控制实体是否可供输入的布尔表达式,并且 DEL 变得越来越繁重。当他们开始要求条件控制和循环时,很明显,我们需要一门真正的编程语言。

大约在同一时间,我们开始为 PETROBRAS 参与另一个项目,称为 PGM,这是一个用于岩性剖面的可配置报告生成器。正如名称所暗示的那样,此程序生成的报告具有高度可配置性:用户可以创建和定位轨道、选择颜色、字体和文本;每个轨道可能都有一个网格,它也有一组选项(日志/线性、垂直和水平刻度等);每条曲线都有自己的比例,在溢出时必须自动更改;等等。

所有这些配置都应由最终用户(通常是地质学家或工程师)完成,并且该程序应在小型机器上运行,例如运行 MS-DOS 的 PC。我们决定通过一种专门的描述语言(我们称之为 Sol)来配置此应用程序,这是最佳方式:Sol 是 Simple Object Language 的缩写,在葡萄牙语中也表示“太阳”。

由于报告生成器有许多不同的对象,每个对象都有许多不同的属性,因此我们没有在语言中修复这些对象和属性。相反,该语言允许类型声明。解释器的主要任务是读取描述,检查给定的对象和属性是否具有正确的类型,然后将信息呈现给主程序。为了允许主程序和解释器之间的这种通信,后者被实现为 C 库,并链接到主程序。因此,主程序可以通过此库中的 API 访问所有配置信息。此外,程序可以为每种类型注册一个回调函数,以便解释器在创建给定类型的对象时调用此函数。

以下代码块显示了 Sol 中的典型代码片段

   -- defines a type `track', with numeric attributes `x' and `y',
   -- plus an untyped attribute `z'. `y' and `z' have default values.
   type @track { x:number,y:number= 23, z=0}

   -- defines a type `line', with attributes `t' (a track),
   -- and `z', a list of numbers.
   -- `t' has as default value a track with x=8, y=23, and z=0.
   type @line { t:@track=@track{x=8},z:number*}

   -- creates an object `t1', of type `track'
   t1 = @track { y = 9, x = 10, z="hi!"}

   -- creates a line `l', with t=@track{x=9, y=10},
   -- and z=[2,3,4] (a list)
   l = @line { t= @track{x=t1.y, y=t1.x}, z=[2,3,4] }
Sol 的语法深受 BiBTeX [21] 和 UIL(用户界面语言)的影响,UIL 是 Motif [24] 中用于描述用户界面的语言。

1993 年 3 月,我们完成了 Sol 语言的第一个实现,但从未交付。到 1993 年年中,我们意识到 DEL 和 Sol 可以合并成一种更强大的语言。用于可视化岩性剖面的程序很快将需要过程编程的支持,以允许创建更复杂的布局。另一方面,数据输入程序还需要描述性功能来对其用户界面进行编程。

因此,我们决定我们需要一种真正的编程语言,包括赋值、控制结构、子例程等。该语言还应提供数据描述功能,例如 Sol 提供的功能。此外,由于该语言的许多潜在用户不是专业程序员,因此该语言应避免使用晦涩的语法(和语义)。最后,新语言的实现应该是高度可移植的。

可移植性要求被证明是其主要优势之一:这两个应用程序应完全可移植,语言也应如此。国有石油公司巴西石油公司无法选择特定硬件,因为它只能根据非常严格的公共资金支出规则购买设备。因此,巴西石油公司拥有种类繁多的计算机,因此在 TeCGraf 为巴西石油公司开发的软件应在其拥有的每台机器上运行,其中包括 PC DOS、Windows(当时为 3.1)、Macintosh 和所有版本的 Unix。

在这一点上,我们可以采用现有的语言,而不是创建新的语言。主要竞争者是 Tcl [25],以及远落其后的 Forth [26] 和 Perl [30]。Perl 不是扩展语言。1993 年,Tcl 和 Perl 仅在 Unix 平台上运行。所有这三种语言的语法都非常晦涩。而且它们都没有为数据描述提供良好的支持。因此,我们开始研究一种新语言。

我们很快意识到,就我们的目的而言,该语言不需要类型声明。相反,我们可以使用该语言本身编写类型检查例程,前提是该语言提供了基本的反射功能(例如运行时类型信息)。像

   t1 = @track {y = 9, x = 10, z="hi!"}
这样的赋值在 Sol 中有效,在新语言中也同样有效,但含义不同:它使用给定字段创建一个对象(即关联表),然后调用函数 track 来验证对象(并最终提供默认值)。

由于新语言是 Sol(sun)的修改版本,TeCGraf 的一位朋友建议使用 Lua(葡萄牙语中的moon),于是 Lua 诞生了。

Lua 从 Sol 继承了记录和列表构造的语法,但它使用关联表统一了它们的实现:记录使用字符串(字段名)作为索引;列表使用整数。除了这些数据描述功能之外,Lua 没有新的概念;我们只想要一种轻量级的通用语言。因此,我们采用了一组小的控制结构,其语法借鉴了 Modula(whileifrepeat until)。从 CLU 中,我们采用了函数调用的多重赋值和多重返回(比输入输出或引用参数更清晰的概念)。从 C++ 中,我们采用了允许仅在需要时声明局部变量的简洁想法。

为数不多的(相当小的)创新之一是字符串连接语法。由于该语言允许将字符串强制转换为数字,因此 + 信号会产生歧义;因此,我们为该操作创建了 ..(两个点)语法。

一个有争议的点是关于分号的使用。我们认为,对于具有 FORTRAN 背景的工程师来说,要求使用分号可能会有点令人困惑,但如果不允许使用分号,可能会让具有 C 或 Pascal 背景的工程师感到困惑。最后,我们决定使用可选分号(一个典型的委员会解决方案)。

最初,Lua 语言有七种类型:数字(实现为浮点数)、字符串、(关联)表、nil(一种具有唯一值且也称为 nil 的类型)、userdata(一个通用 C 指针,用于表示 Lua 中的 C 结构)、Lua 函数和 C 函数。(经过八年的持续演变,Lua 类型中唯一的变化是将 Lua 函数和 C 函数统一为单一的函数类型。)为了保持语言精简,我们没有包含布尔类型。与 Lisp 中一样,nil 表示 false,任何其他值都表示 true。这是我们今天有时会后悔的少数经济之一。

Lua 还从 Sol 继承了作为库实现的概念。该实现遵循极限编程现在支持的一个原则:“最简单、最可能起作用的东西”[1]。我们对扫描器使用 lex,对解析器使用 yacc。解析器将程序翻译成字节码形式,然后由一个基于堆栈的简单解释器执行。该语言有一个非常小的预定义库,因为它很容易在 C 中添加新函数。

尽管有这种简单的实现——或者可能正是因为如此——Lua 超出了我们的预期。PGM 和数据输入 (ED) 项目都成功使用了 Lua [16](PGM 至今仍在使用)。很快,TeCGraf 内部其他项目也开始使用 Lua。

最初几年(1994-1996)

新用户会提出新需求。毫不奇怪,对 Lua 的首批需求之一就是提高性能。将 Lua 用于数据描述对典型的脚本语言提出了不同寻常的挑战。

我们刚开始使用 Lua 时,就发现了它作为图形元文件的支持语言的潜在用途。Lua 的数据描述功能允许将其用作图形格式。与其他可编程元文件相比,Lua 元文件具有基于真正过程语言的优势。例如,VRML 格式使用 Javascript 对过程对象进行建模,从而导致异构(因此不干净)的代码 [2]。使用 Lua,在场景描述中合并过程对象是自然的。过程代码片段可以与声明语句结合使用来对复杂对象进行建模,同时保持清晰度。

数据输入程序 (ED) 是第一个将其图形元文件用于 Lua 的程序。一张图表有几千个部件并不少见,这些部件用一个包含几千个项目的 Lua 构造函数在数百 KB 的文件中进行描述。这意味着从编程语言的角度来看,Lua 必须处理大型程序和大型表达式。而且,由于 Lua 即时编译此类程序(“即时编译器”),这也意味着 Lua 编译器必须非常快。追求性能的第一个牺牲品是 lex。用手工编写的扫描器替换 lex 生成的扫描器几乎将 Lua 编译器的速度提高了一倍。

我们还为构造函数创建了新的操作码。类似于以下内容的列表构造函数的原始代码

   @[30, 40, 50]
类似于以下内容
   CREATETABLE
   PUSHNUMBER 1                 # index
   PUSHNUMBER 30                # value
   SETTABLE
   PUSHNUMBER 2                 # index
   PUSHNUMBER 40                # value
   SETTABLE
   PUSHNUMBER 3                 # index
   PUSHNUMBER 50                # value
   SETTABLE
使用新方案,代码如下所示
   CREATETABLE
   PUSHNUMBER 30                # value
   PUSHNUMBER 40                # value
   PUSHNUMBER 50                # value
   SETTABLE 1 3                 # set elements from index 1 to 3
对于一个较长的构造函数,不可能在存储所有元素之前将它们全部推入堆栈;因此,代码生成器会不时发出 SETTABLE 指令,以刷新堆栈。

(从那时起,我们一直尝试改善编译时间。如今,Lua 编译一个包含 30000 个赋值的程序的速度比 Perl 快六倍,比 Python 快八倍。)

1994 年 7 月,我们发布了带有这些优化的 Lua 新版本,名为 Lua 1.1 [19]。此版本可通过 ftp 下载。之前的版本从未公开发布,然后取名为 Lua 1.0。大约在那个时候,我们还发表了第一篇描述 Lua 的论文 [10]。

Lua 1.1 具有限制性的用户许可证。它可免费用于学术用途,但不能用于商业用途。(尽管有许可证,它始终是开源的。)但该许可证不起作用。大多数竞争对手,如 Perl 和 Tcl,都是免费的。此外,商业限制甚至会阻碍学术用途,因为一些学术项目计划最终进入市场。因此,我们发布了下一版本的语言 Lua 2.1,作为免费软件。

Lua 版本 2

Lua 2.1(于 1995 年 2 月发布)带来了许多重要的变化。其中之一不在语言本身,而是在语言开发过程中:我们决定我们应该始终尝试改进语言,即使是以与以前版本略微不兼容为代价。

在版本 2.1 中,我们实际上引入了与版本 1.1 的重大不兼容性(但我们提供了一些工具来帮助转换)。我们从构造函数中删除了 @,并统一了记录和大括号的使用。删除 @ 是一个微不足道的更改,但它实际上改变了语言的感觉,而不仅仅是它的外观。

更重要的是,我们简化了构造函数的语义。在 Lua 1.1 中,表达式 @track{x=1, y=10} 有特殊的含义。在 Lua 2.1 中,表达式 track{x=1, y=10} 只是 track({x=1, y=10}) 的语法糖,即它创建一个新表并将其作为唯一参数传递给函数 track

从一开始,我们就将 Lua 设计为一种扩展语言,从某种意义上说,C 程序可以注册自己的函数,以便从 Lua 中透明地调用它们。通过这种方式,可以轻松地使用特定于领域的基元扩展 Lua,以便最终用户使用适合其需求的语言。

在版本 2.1 中,我们引入了后备的概念:每当 Lua 不知道如何继续时调用的用户定义函数。然后,Lua 成为一种可以通过两种方式扩展的语言:通过扩展其“基元”函数集和通过后备扩展其语义。这就是我们现在称 Lua 为可扩展扩展语言的原因。

我们定义了算术、比较、字符串连接、表访问等的后备。当由用户设置时,每当这些操作的操作数不是必需的类型时,就会调用相应的后备函数。例如,每当添加两个值且其中一个不是数字时,就会调用后备,其返回值用作加法的结果。

特别感兴趣的——实际上是引入后备的主要原因——是表访问的后备:在语句 x=a[i] 中,如果 a[i] 为 nil,则会调用后备(如果已设置),并使用其返回值作为 a[i] 的值。这个简单的特性允许程序员为表访问实现不同的语义。特别是,可以实现多种继承,最简单的继承是通过委托的单一继承

   function Index (a,i)
     if i == "parent" then       - to avoid loop
       return nil
     end
     local p = a.parent
     if type(p) == "table" then
       return p[i]               - may trigger Index again
     else
       return nil
     end
   end

   setfallback("index", Index)

此代码沿“父级”链向上查找,直到表具有必需的字段或链结束。通过如上设置“索引”后备,即使 b 没有 color 字段,以下代码也会打印 red

   a=Window{x=100, y=200, color="red"}
   b=Window{x=300, y=400, parent=a}
   print(b.color)

通过“父级”字段进行委托没有任何神奇或“硬编码”之处。这是一个程序员选择。她可以使用不同的名称来表示“父级”字段,或者通过允许“父级”字段本身成为按顺序尝试的父级表来实现更复杂的多重继承,或者其他任何方式。

a 不是一个表时,针对表达式 a[i] 调用了不同的后备。有一个“可获取”后备,触发以获取 a[i] 的值,如 x=a[i];还有一个“可设置”后备,触发以设置 a[i] 的值,如 a[i]=x

利用这些表后备有很多可能性;跨语言继承是一种非常强大的可能性:当 a 是一个用户数据值(宿主 C 程序中的指针)时,表后备可以使程序员透明地访问驻留在宿主程序中的数据结构中的值。

我们决定不硬编码任何这些可能的行为,这导致了 Lua 的一个主要设计概念:元机制。我们没有在语言中添加大量特性,而是提供了方法,以便用户可以自己编程特性,按照她想要的方式,并且针对她需要的那些特性。

后备元机制允许 Lua 支持面向对象编程,因为可以实现(多种类型的)继承(以及运算符重载)。我们甚至添加了一点语法糖来定义和使用“方法”:函数可以定义为 a:f(x,y,z),并且向 a.f 添加了一个名为 self 的隐藏参数,这意味着对 a:f(10,20,30) 的调用等效于 a.f(a,10,20,30)

1996 年 5 月,我们发布了 Lua 2.4。此新版本的一个主要特性是一个外部编译器,称为 luac。此程序预编译 Lua 代码,并将字节码和字符串表保存到二进制文件中。此文件的格式被选为易于加载且可在不同平台之间移植。使用 luac,程序可以避免在运行时进行解析和代码生成,这可能是代价高昂的,特别是对于大型静态程序(例如图形元文件)。

我们关于 Lua 的第一篇论文 [10] 已经预见了外部编译器的可能性,但我们仅在 Lua 在 TeCGraf 被广泛使用并且大型图形元文件以图形编辑器的输出形式用 Lua 编写后才需要它。

除了加载速度更快之外,luac 还允许离线语法检查和保护源代码免受用户更改。但是,预编译并不意味着执行速度更快,因为 Lua 块在执行之前始终被编译为字节码 – luac 只是允许将这些字节码保存到文件中以供以后执行。

luac 以“客户端模式”实现,也就是说,它使用实现 Lua 的模块,仅仅作为(礼貌的)客户端,即使它确实包含私有头文件以访问需要保存的内部数据结构。此策略的一个优点是,它有助于将 Lua 内核的实现构建为清晰分离的模块。特别是,现在很容易移除解析模块(词法分析器、解析器和代码生成器),它们在 Lua 4.0 中占内核代码的 40%,仅留下加载预编译块的微小模块。这对于嵌入在移动设备或机器人等小型设备中的 Lua 微小实现非常有用(Crazy Ivan,一个在 2000 年和 2001 年在丹麦赢得 RoboCup 的机器人,有一个用 Lua 实现的“大脑”)。

国际曝光(1996-2000)

1996 年 6 月,我们在《软件:实践与经验》[18] 中发表了一篇关于 Lua 的学术论文。1996 年 12 月,《Dr. Dobb's》杂志刊登了一篇关于 Lua 的文章 [11]。这些针对不同社区的出版物开始让 Lua 在国际上闻名。

在 Dr. Dobb's 发表文章后不久,我们收到了几条关于 Lua 的消息。第一条消息如下

   From: Bret Mogilefsky <[email protected]>
   To: "'[email protected]'" <[email protected]>
   Subject: LUA rocks!  Question, too.
   Date: Thu, 9 Jan 1997 13:21:41 -0800

   Hi there...

   After reading the Dr. Dobbs article on Lua I was very eager to check it
   out, and so far it has exceeded my expectations in every way!  It's
   elegance and simplicity astound me.  Congratulations on developing such
   a well-thought out language.

   Some background: I am working on an adventure game for the LucasArts
   Entertainment Co., and I want to try replacing our older adventure game
   scripting language, SCUMM, with Lua.

   [...]
事实证明,Bret Mogilefsky 是 Grim Fandango 的首席程序员,这是 LucasArts 在 1997 年发布的主要冒险游戏。在另一条消息中,他告诉我们“这款游戏的巨大内容是用 Lua 编写的”(他的重点)。Lua 在游戏中首次使用吸引了世界各地许多游戏开发者的关注。不久之后,Lua 开始频繁出现在游戏新闻组中,例如 rec.games.programmercomp.ai.games

由于其小巧、性能良好、可移植且易于集成,Lua 在扩展游戏中获得了极大的欢迎。如今,几家游戏公司都在使用 Lua(例如,LucasArts、BioWare、Slingshot Game Technology 和 Loewen Entertainment),并且对 Lua 的了解是游戏行业的一项可销售技能。我们估计,一半的 Lua 用户在某种程度上参与了游戏编程,但很难说得更具体,因为游戏行业有很多秘密。例如,Bret Mogilefsky 为 Grim Fandango 改编了 Lua,但细节当然属于专有信息。

将脚本语言嵌入游戏有很多好处。脚本语言可用于定义精灵和对象物理、管理对象 AI 和角色控制,以及处理输入设备事件。例如,引擎可能不知道“伤害”、“速度”、“武器”等内容。选择一种简单的语言还可以让游戏设计师使用可编程工具。这对于游戏开发至关重要,因为设计师可以试验他们的创作。脚本语言还允许快速原型制作,并有助于实现调试器工具。

最近,2000 年,LucasArts 发布了另一款使用 Lua 的游戏:猴岛逃亡,这是猴岛系列冒险游戏的第四部。在这款游戏中,为了向 Lua 致敬,他们将游戏中的一个酒吧从SCUMM(他们之前使用的语言)更名为Lua 酒吧。

除了在电脑游戏中广泛使用(例如,Grim Fandango、博德之门、MDK2、猴岛逃亡)之外,Lua 还被世界各地许多不同领域所使用。

Lua 最早的外部使用之一是在史密森天体物理台。他们设计了一个广义孔径程序来模拟物理障碍物对入射光子流的影响,并使用 Lua 对几何形状和入射光子流与孔径的相互作用进行建模 [23]。该程序是支持 AXAF 计划(先进 X 射线天体物理设施)的一部分,该计划是 NASA 四大太空观测站中的第三个。

Performance Technologies 使用 Lua 来实现 CPC4400 的命令行界面,CPC4400 是一款可热插拔的以太网交换机。通过将 Lua 公开为 CPC4400 的脚本语言,用户可以将事件(例如链路状态、拓扑更改检测和 RMON 警报)与 Lua 脚本关联起来。

Tollgrade Communications 在其下一代电话网络测试产品DigiTest中使用了 Lua。Lua 用于用户界面、自动化测试脚本和结果分析。

Lua 还用于巴西的 InCor 心脏研究所(Instituto do Cora��o,圣保罗);巴西的 CEPEL(国家电力公司 ELETROBRAS 的研究中心);柏林的魏尔斯特拉斯研究所;柏林工业大学;以及许多其他地方。

1998 年,Cameron Laird 和 Kathryn Soraiz 在 SunWorld 杂志上发表了关于脚本语言的专栏,他们估计“世界上可能只有几万名 Lua 程序员” [20]。他们认为“用户群小”,而我们认为这是该语言越来越受欢迎的有力标志。

Lua 3 版

Lua 3.0(1997 年 7 月)用更强大的标记方法概念取代了后备。后备本质上是全局的:每次发生事件时都会调用用户函数,并且每个事件只有一个函数。例如,这使得难以组合具有不同继承概念的 Lua 模块。虽然可以链接后备,但链接速度慢且容易出错,实际上没有人这样做。

自 Lua 3.0 起,程序员可以创建标记,并将表和用户数据与标记关联。标记方法本质上是根据运算符的标记选择的回退。通过标记和标记方法,不同的表(和用户数据)可能对其操作具有不同的回退。

标记概念本质上为 Lua 提供了用户定义的类型。换句话说,标记只是一个表示新类型的数字。当将表与特定标记关联时,实际上是在为该表定义一个新类型:表的类型(或标记)指定了它如何实现其运算符。

当我们引入回退时,它们中的大多数都描述了 Lua 在其他错误事件中的行为,例如对非表值进行索引或调用非函数值。因此,我们将回退视为一种异常处理机制。随着用户定义标记的引入,回退(现在称为标记方法)主要成为描述新类型行为的机制,尽管我们仍然可以使用它们来扩展基本类型的行为。

尽管有这种新状态,但很长一段时间以来,我们仍然将标记方法视为异常处理机制,并且没有将标记与类型联系起来。直到最近,我们才意识到用户定义标记和标记方法作为创建用户定义类型的机制的全部意义。Lua 4.1 将通过允许用户为这些新类型提供名称来确认这一认识(目前,只有基本类型有名称)。

Lua 3.0 还以类似 C 的预处理器的形式带来了对条件编译的支持。与任何语言特性一样,添加它太容易了(尽管它确实使词法分析器复杂化),很快程序员就开始使用它(程序员会使用任何语言特性)。一旦开始使用某个特性,对更多功能的需求就会紧随其后。最频繁的要求之一是添加宏扩展,但从邮件列表和我们自己之间的长时间讨论中没有出现明确的提议。每项此类提议都意味着词法分析器和解析器的重大更改,并且好处并不明显。因此,预处理器从 Lua 3.0 到 3.2 版本(两年)保持不变。

最终我们决定预处理器弊大于利,它让代码变得更加庞大,并引发用户无休止的讨论,因此我们在 Lua 4.0 中将其删除。我们觉得没有预处理器的 Lua 现在更简洁了。多年来,我们一直致力于让 Lua 变得更简单,并删除了我们曾经视为特性但实际上很少有程序员使用,后来被认为是缺陷的语言中的晦涩部分。

Lua 版本 4

在 3.2 版之前,一次只能激活一个 Lua “状态”。我们确实有一个用于更改状态的 API 函数,但使用起来有点别扭。为了简单起见,我们在设计 API 时没有在函数中包含显式状态参数——只有一个全局状态。回想起来,这是一个错误。当 Lua 3.2 发布时,很明显,如果能够以一种便捷的方式运行多个 Lua 状态,许多应用程序会变得更简单。例如,我们必须制作一个 Lua 3.2 的特殊版本,以包含在 CGILua 中,后者是 Web 浏览器的一个扩展,用于在 Lua 中提供动态页面服务和 CGI 编程。早些时候,LucasArts 为 Lua 3.1 做了类似的事情。

当我们讨论 Lua 3.3 的计划时,带有显式状态的 API 是我们的首要任务。然而,这引发了兼容性问题。最终,由于必须存在一些不兼容性,我们决定重写 API,下一个版本成为 Lua 4.0。现在,API 不仅包含显式状态,而且还更易于使用且更高效。我们担心向新 API 的过渡会有点痛苦,因为这是我们自 Lua 1.1 以来首次真正更改 API。我们在邮件列表中确实收到了一些抱怨,但总体而言,这次更改一点也不痛苦。大多数 Lua 程序员不接触 API,许多人只通过自动工具使用它,许多人认为好处超过了过渡成本。

我们在 2000 年 11 月发布了 Lua 4.0。除了新的 API 之外,此版本还带来了许多其他小改进,其中包括 for 循环。

每个从事编程语言工作的人都知道人们很容易就这个话题开始“宗教战争”。这些战争的一个有趣特点是,通常,主题越普通,讨论就越激烈。例如,人们对分号的讨论比对高阶函数的讨论要兴奋得多。当然,其中一个原因是,对前者有意见的人比对后者多得多。但另一个更重要的原因是,普通细节对人们对语言的舒适度有很大影响。如果一个工具没有良好的抓地力,那么创建一个奇妙的、经过深思熟虑的工具是没有用的——没有人会使用它。

自 1.1 版以来,for 语句一直出现在大多数 Lua 用户的愿望清单中。最常见的抱怨是人们忘记在 while 循环的末尾编写增量,从而导致无限循环。

我们很快就达成了一致。但是,尽管我们都同意需要一个 for 循环,但我们无法就任何特定结构达成一致。我们认为类似 Pascal(或类似 Modula)的结构过于严格,因为它不考虑对表元素或文件行的迭代。此外,将标识符 to 变成保留字将是不可接受的不兼容。另一方面,C 传统中的 for 循环不适合 Lua。

随着在 3.1 版(98 年 7 月)中引入闭包和匿名函数,我们决定使用高阶函数进行迭代。(事实上,迭代器的需求是 Lua 中引入匿名函数的主要原因之一。)Lua 3.1 推出了两个用于迭代的预定义函数

  foreach(table, f)
  foreachi(table, f)
foreach 函数对给定表中的所有键值对应用 f,顺序不固定。foreachi 函数类似,但它将表视为一个列表(或数组):它只遍历具有数字索引的元素,并确保按升序遍历。尽管我们只为这两个特定遍历提供了函数,但创建新的迭代器非常容易。

在引入这些迭代函数两年多后,我们意识到,尽管创建新的迭代器很容易,但几乎没有人这样做。第一个原因是大多数程序员对匿名函数和高阶函数不适应,尤其是在过程语言中。但第二个,也是我们看来最重要的原因是,几乎没有人需要其他迭代器。这意味着,多年来,我们一直在尝试实现一个真正的用户并不真正关心的正交性。有了这个理解,我们很快设计了一个 for 循环,有两种格式,一种用于数字循环,另一种用于遍历表。

for 语句是自其第一个版本以来语言中最成功的更改之一。首先,它确实涵盖了大多数常见循环;对于真正通用的循环,有 while 循环。其次,由于其严格的格式,很容易创建特定的操作码来实现循环,因此具有空体的数字 for 循环运行速度比等效的 while 结构快两倍以上。

结论

目前,Lua 拥有一个成熟的用户群。Lua 有一个活跃的讨论列表,来自 30 多个不同国家的近 500 人参与。其网站(www.lua.org)每天收到来自 50 个国家的约 500 次访问。其用途从冒险游戏到网络服务器,再到电话网络测试和以太网交换机。

多个 ftp 站点提供原始 Lua 源代码,还有多个其他站点分发特定平台的版本,例如 Windows 的 DLL、EPOC 的 SIS、Linux 的 RPM、RISC OS 的二进制文件等。此外,多本杂志已在补充光盘中分发了 Lua(例如 Dr. Dobb's、Linux Magazine France 和日本的 C Magazine)。

作为一门语言,Lua 的主要贡献源于决定提供元机制而不是功能。作为一种产品,Lua 的成功源于其简单性、小巧性以及实现的可移植性,这使得 Lua 可以在许多不同的平台中使用,包括小型设备,如掌上电脑、手持设备、专用电路板和机器人。Cameron Laird 和 Kathryn Soraiz 在 1998 年预测,“无处不在的嵌入式处理(汽车、管道和厨房电器中的计算机)即将爆发,这只会对 Lua 有利”[20]。当时,我们并没有太注意,但他们是对的。

Lua 还通过多篇有关 Lua 的论文和使用 Lua 作为相关技术的论文做出了多项学术贡献 [4,29,14,17,13,28,15,22,6,27,9,5,7,8].

成功是有代价的。随着语言的发展,与之前版本的兼容性越来越成为创新的障碍。尽管如此,我们不允许兼容性阻碍进步;它只是语言设计炼金术中的一种成分(尽管是一种强大的成分)。

最后,保留一门语言远不止设计它。在各个方面都必须完全注重细节:语言设计、实现、文档、建立用户社区并倾听他们的意见,同时坚持最初的设计决策。

致谢

如果没有许多人的帮助,Lua 永远不会成为现在的样子。TeCGraf 中的每个人都以不同的形式做出了贡献——使用该语言、讨论它、在 TeCGraf 外传播它。特别感谢 TeCGraf 的负责人 Marcelo Gattass,他一直鼓励我们,并给了我们对语言及其实现的完全自由。Lua 实际上是第一款在互联网上公开发布的 TeCGraf 产品,甚至早于繁荣时期。

如果没有用户,Lua 将只是另一种语言。用户及其用途是对一门语言的终极考验。从他们那里,我们获得了错误报告、设计缺陷、看待现实的新方式。特别感谢讨论列表的成员,感谢他们的建议、抱怨,以及主要感谢他们忍受我们有时专横的风格。

参考文献

[1] K. Beck。极限编程详解:拥抱变化。艾迪生-韦斯利,2000 年。

[2] G. Bell、R. Carey 和 C. Marrin。虚拟现实建模语言规范——版本 2.0。 http://www.vrml.org/VRML2.0/FINAL/,1996 年 8 月。(ISO/IEC CD 14772)。

[3] T. J. Bergin 和 R. G. Gibson,编辑。编程语言历史,第 2 卷。ACM 出版社,1996 年。

[4] A. Carregal 和 R. Ierusalimschy。Tche——Lua 语言的可视化环境。在第八届巴西计算机图形学研讨会上,第 227-232 页,圣卡洛斯,1995 年。

[5] W. Celes。分层平面细分的可配置建模。博士论文,巴西里约热内卢天主教大学计算机系,巴西里约热内卢,1995 年。

[6] R. Cerqueira、C. Cassino 和 R. Ierusalimschy。跨不同组件软件系统的动态组件粘合。在DOA'99——分布式对象和应用程序国际研讨会上,第 362-371 页,苏格兰爱丁堡,1999 年。IEEE 计算机协会。

[7] M. T. M. de Carvalho。计算机力学中可配置应用程序开发的策略。博士论文,巴西里约热内卢天主教大学土木工程系,巴西里约热内卢,1995 年 6 月。

[8] R. F. de Gusm�o Cerqueira。软件组件系统之间的动态组合模型。博士论文,巴西里约热内卢天主教大学计算机系,巴西里约热内卢,2000 年 8 月。

[9] M. J. de Lima、N. Rodriguez 和 R. Ierusalimschy。分布式对象系统中作为一等值的远程函数。在第四届巴西编程语言研讨会上,第 1-14 页,累西腓,2000 年 5 月。

[10] L. H. Figueiredo、R. Ierusalimschy 和 W. Celes。扩展应用程序语言的设计和实现。在第二十一届半导体研讨会上,第 273-284 页,卡桑布,1994 年。

[11] L. H. Figueiredo、R. Ierusalimschy 和 W. Celes。Lua:一种可扩展的嵌入式语言。Dr. Dobb's Journal,21(12):26-33,1996 年 12 月。

[12] L. H. Figueiredo、C. S. Souza、M. Gattass 和 L. C. G. Coelho。用于捕获绘图数据的界面生成。在SIBGRAPI '92(巴西计算机图形学和图像处理研讨会)论文集中,第 169-175 页,1992 年。

[13] P. R. Gomes、B. Feij�、R. Cerqueira 和 R. Ierusalimschy。虚拟原型中的反应性和主动性。在 I. Horvath 和 A. Taleb-Bendiab 编辑的第二届并发工程工具和方法国际研讨会中,第 242-253 页,英国曼彻斯特,1998 年 4 月。

[14] T. G. Gorham 和 R. Ierusalimschy。一个扩展语言的反射式调试系统。在 R. Bigonha 编辑的第一届巴西编程语言研讨会中,第 103-114 页,贝洛奥里藏特,1996 年 9 月。

[15] A. Hester、R. Borges 和 R. Ierusalimschy。使用 Lua 构建灵活且可扩展的 Web 应用程序。通用计算机科学杂志,4(9):748-762,1998 年。http://medoc.springer.de:8000/.

[16] R. Ierusalimschy、W. Celes、L. H. Figueiredo 和 R. de Souza。Lua:一种应用程序自定义语言。在第七届巴西软件工程研讨会 - 工具手册中,第 55 页,巴西里约热内卢,1993 年。

[17] R. Ierusalimschy、R. Cerqueira 和 N. Rodriguez。使用反射性与 CORBA 交互。在IEEE 国际计算机语言会议 (ICCL'98)中,第 39-46 页,伊利诺斯州芝加哥,1998 年 5 月。IEEE 计算机协会。

[18] R. Ierusalimschy、L. H. de Figueiredo 和 W. Celes。Lua - 一种可扩展的扩展语言。软件:实践与经验,26(6):635-652,1996 年。

[19] R. Ierusalimschy、L. H. Figueiredo 和 W. Celes。编程语言 Lua 的参考手册。计算机科学专著 3/94,里约热内卢天主教大学,巴西里约热内卢,1994 年。

[20] C. Laird 和 K. Soraiz。1998 年:脚本编写的突破之年。SunWorld,1998 年 8 月。 http://sunsite.icm.edu.pl/sunworldonline/swol-08-1998/swol-08-regex.html

[21] L. Lamport。LaTeX:文档准备系统。艾迪生-韦斯利,1986 年。

[22] M. C. Martins、N. Rodriguez 和 R. Ierusalimschy。CORBA 服务器的动态扩展。在Euro-Par'99 并行处理中,第 1369-1376 页,法国图卢兹,1999 年。施普林格出版社。(LNCS 1685)。

[23] D. Nguyen、T. Gaetz、D. Jerius 和 I. Stern。使用广义孔径程序对 AXAF 障碍进行建模。在天文数据分析软件和系统 VI中,第 485-487 页,1996 年 9 月。

[24] 开放软件基金会。OSF/Motif 程序员指南。Prentice-Hall, Inc.,1991 年。(ISBN 0-13-640673-4)。

[25] J. Ousterhout。Tcl:一种可嵌入的命令语言。在1990 年冬季 USENIX 会议论文集中。USENIX 协会,1990 年。

[26] E. D. Rather、C. H. Moore 和 D. R. Colburn。Forth 的演变。在ACM SIGPLAN 编程语言历史会议中,1993 年 4 月。

[27] N. Rodriguez 和 R. Ierusalimschy。基于 CORBA 的应用程序的动态重新配置。在 J. Pavelka、G. Tel 和 M. Barto\vsek 编辑的SOFSEM'99:第 26 届信息学理论与实践当前趋势会议中,第 95-111 页,捷克共和国米洛维,1999 年。施普林格出版社。(LNCS 1725)。

[28] N. Rodriguez、R. Ierusalimschy 和 R. Cerqueira。使用 CORBA 组件进行动态配置。在第 4 届可配置分布式系统国际会议 (ICCDS'98) 中,第 27-34 页,马里兰州安纳波利斯,1998 年 5 月。IEEE 计算机协会。

[29] N. Rodriguez、C. Ururahy、R. Ierusalimschy 和 R. Cerqueira。使用解释型语言在分布式系统上实现并行算法。在 L. Bougé、P. Fraigniaud、A. Mignotte 和 Y. Robert 编辑的Euro-Par'96 并行处理——第二届 Euro-Par 国际会议 中,第 597-600 页,第一卷,法国里昂,1996 年 8 月。施普林格出版社。(LNCS 1123)。

[30] L. Wall 和 R. L. Schwartz。Perl 编程。O'Reilly & Associates, Inc.,1991 年。

[31] R. L. Wexelblat 编辑。编程语言历史。学术出版社,1981 年。