转载出处:http://my.oschina.net/xhan/blog/309613
说明: 这个文档是 Lua1.1 的 doc 目录里的 lua.ps 文件。
同时这个文档可以这里找到:http://www.lua.org/semish94.html
原文版权归原作者所有,这篇翻译只是作为学习之用。如果翻译有不当之处,请参考原文。
--------------------以下是正文------------------
应用程序扩展语言的设计和实现
摘要。我们描述 Lua 的设计和实现,一个简单而强大的应用程序扩展语言。尽管Lua是一种程序语言,它有数据描述能力,并已广泛应用于几个生产任务,包括用户配置,通用的数据输入,用户界面的描述,描述应用程序对象和存储结构化图形元文件。
--------------------------------------
简介
--------------------------------------
有越来越多的定制应用程序的需求。随着应用程序变得更加复杂,用简单参数来定制变得不太可能:用户现在想要在程序运行时决定采用哪种配置;用户还想自己写宏和脚本,以提高生产率(Ryan 1990)。因此,如今规模大点儿的应用程序几乎总是为终端用户提供配置或脚本语言以做扩展之用。这些语言通常是简单的,但每一种都有它自己的语法。因此,用户必须为每种应该程序学会一门新的语言(开发人员也不得不为每种应用程序设计、实现和调试一门新的语言)。
我们第一个专有脚本语言经验来自于一个数据输入应用程序,我们为这个程序设计了一个很简单的声明式语言(Figueiredo–Souza–Gattass–Coelho 1992)。(数据输入领域对于用户自定义的动作有迫切需要,因为事先编码的确认测试不可能覆盖所有应用程序)。当用户要求为这门语言添加越来越多功能的时候,我们认定需要一个更通用的语言并开始设计一个通用的嵌入式语言。与此同时,另一个声明式语言被添加到一个不同的应用程序中以做数据描述之用。因此,我们决定将这两种语言合二为一,并为 Lua 程序语言添加了数据描述功能。Lua 已经超越了原本的目的并被用于其他工业项目。
本文档介绍 Lua 的设计决策及实现细节。
--------------------------------------
扩展语言
--------------------------------------
现在认为应用程序扩展语言的使用是一个重要的设计技术:它使应用程序的设计变得更清晰,并为用户提供个性化配置。由于大多数扩展语言是简单,专门针对某个任务的,他们被称为“小语言“(Bentley 1986; Valdés 1991)。与之对应的是编写应用程序的"大"语言,主流语言。如今这种区别并不明显,因为实际上多个应用程序的主要部分就是由扩展语言写成的。扩展语言有几种:
配置语言:参数选择,通常实现为命令行参数列表或从配置文件中读取键值对(例如:DOS 的 config.sys, MS-Windows 的 .ini文件, X11 的资源文件, Motif 的 UIL 文件); 脚本语言:自动化任务,有限的流程控制,如用于 DOS 的批处理文件或各种 Unix shell;
宏语言:也为自动化任务,但通常只作为一系列顺序执行的基本操作,没有流程控制;
嵌入式语言:基于应用程序提供的接口,用户可以自已定制函数来扩展应用程序。这些语言通常是被简化的主流编程语言如 LISP 和 C的变体,具有很强大的功能。
嵌入式语言不同于独立的语言之处是嵌入式语言只能嵌入到宿主才能工作,因为被称为嵌入式编程。此外,宿主程序通常为嵌入式语言提供某特定领域的扩展,通过提供更高层次的抽象,创建一个特定版本的嵌入式语言。为此,一个嵌入式语言既有它自己的编程语法又有与宿主交互的 API。因此,不像那样用来给宿主提供参数值或一系列顺序操作的简单扩展语言,嵌入式语言和宿主程序之间有一个双向的通信。注意,应用程序员与嵌入式语言交互使用的是编写宿主程序的主流编程语言,而终端用户与与应用程序交互则只使用嵌入式语言。
LISP 通常是扩展语言的一个受欢迎的选择,因其简单,容易解析的语法和内置的可扩展性(Beckman 1991; Nahaboo)。举例来说,Emacs 的主要部分事实上就是由它自己的 LISP 变体写成;其他文本编辑器遵循同样的选择。然而,在用于定制时,LISP 不能称为对用户友好。C 和 shell 也同样不能称为对用户友好,后者的语言甚至更复杂和不常见。
设计 Lua 时的一个基本观点 是它应该有清楚但常见的语法:我们很快为它选择了一个简化了的类 Pascal 语法。 我们避免选择基于 LISP 或者 C 的语法,因为它可能使非程序员用户望而却步。因此,Lua 首先是一种程序语言。然而,如前所述,Lua 所具有的数据描述能力增加了它的表达能力。
--------------------------------------
概念
--------------------------------------
Lua 是一种通用的嵌入式编程语言,支持过程式编程与数据描述功能。做为一个嵌入式语言,Lua 没有 "main" 函数的概念;它只能嵌入到宿主(Lua 做为 C 函数库提供,与宿主应用程序链接)运行。宿主可以执行一段 Lua 代码,可以读写 Lua 变量,可以注册被 Lua 代码调用的 C 函数。通过注册 C 函数,Lua 可以扩展自己以应对不同的领域,从而创建可定制、共享语法框架的编程语言(Beckman 1991)。
本节包含一个 Lua 主要概念的简介。包括一些实际的例子代码,以领略该语言的风格。语言的精确定义可见语言参考手册(Ierusalimschy–Figueiredo–Celes 1994)。
-------------------
语法
-------------------
如前所述,我们明确设计的 Lua 有一个简单,常见的语法。Lua 用隐式但明确结束的块结构支持一组常见的语句。简单的语句包括简单的赋值;控制结构如 while-do-end, repeat-until, if-then-elseif-else-end;函数调用。非常见的语句包括多重赋值;局部变量声明,局部变量可以放在块内的任何地方;表构造函数,可以包括用户自定义的验证函数(见下文)。此外,Lua 函数可以有数量可变的参数,可以返回多个值。这就避免了当需要返回多个结果时通过引用传递参数。
-------------------
环境和模块
-------------------
Lua 中所有的语句都在一个全局环境中执行。这个全局环境持有所有的全局变量和函数,在嵌入语言一开始执行时进行初始化,并持续到结束。这个全局环境可以用 Lua 代码或者嵌入程序来管理,可以通过 Lua 的实现库来读写全局变量。
Lua 的执行单元叫做模块。一个模块可以包含语句和函数定义,可以在一个文件中或者在一个宿主程序的字符串中。当执行一个模块,首先它所有的函数和语句被编译,函数被添加到全局环境,然后语句按顺序执行。模块对于全局环境的所有修改是持久的,这些修改在模块结束后依然可见。修改包括全局变量的修改和新函数的定义(一个函数的定义事实上就是对于一个全局变量的赋值。见下文)
-------------------
数据类型和变量
-------------------
Lua 是动态类型语言:变量没有类型;只有值有类型。所有值含有自己的类型。所有,Lua 语言中没有类型定义。没有变量类型,看起来没有什么,事实上是简化语言的一个重要因素;很多有类型的语言在作为扩展语言进行裁剪时,也经常会把它做为一个重要特性。此外,Lua 有垃圾回收。它跟踪哪些值被使用并丢弃那些不被使用的。这避免了显式内存分配的需要,编程错误的主要来源。Lua 中有七种基本的数据类型的:
nil: 单个值类型 nil;
number: 浮点数;
string: 字符数组;
function: 用户定义的函数;
Cfunction: 宿主程序提供的函数;
userdata: 宿主数据指针;
table: 关联数组.
Lua 提供了一些自动类型转换。 如果可能的话,一个字符串参与数值运算时被转换为数值型。相反,当一个数值被用于本该是字符串的地方的时候,数值被转换为字符串。这是转换是有用的,因为它简化编程并且避免了显式转换函数的需要。
全局变量不需要声明,局部变量才需要。任何变量被假定为是全局的除非显式声明为 local 局部变量。局部变量声明可以放在一个块内的任何地方。因为只有局部变量需要声明,可以把变量的声明放在接近它被使用的地方。因此通常可是简单的判断出一个给定的变量是局部的或全局的。
在第一次赋值之前,变量的值为 nil。因此,Lua 中没有未初始化的变量,编程错误的另一个主要来源。然而,nil 唯一有效的操作是赋值和相等测试(nil 的主要属性是不同于任何其他值)。因此,在本该使用一个“实际”的值的时候使用了“未初始化" 的变量(例如,一个算术表达式)会导致执行错误,提醒程序员没有正确地初始化变量。因此,自动地初始化变量为 nil 的目的不是为了鼓励程序员在使用变量前不对它进行初始化,实际上是为了能让 Lua 指示出使用了未被初始化的变量。
Lua 中函数是第一类值(first-class values):他们可以存储在变量中,做为参数传递给其他函数或者做为结果返回。当函数被定义,它的函数体被编译并保存在一个给定名称的全局变量。Lua 可以调用(和操作)写在 Lua 或 C 中的函数;后者的类型是 Cfunction。
userdata 类型允许 Lua 变量保存任意的 C 指针(void*);在 Lua 中对它有效的操作是分配和相等测试。
table 类型实现为关联数组,即可以用数字和字符串索引的数组。因此,该类型不仅可用于表示普通数组,也可以用于表示符号表,集合,记录等。为表示一个记录,Lua 使用字段名为下标。语言通过提供 a.name 这种表示作为 a["name"] 的语法糖。
关联数组是一个功能强大的语言结构;许多算法得以简化,因为用于搜索他们的所需的数据结构和算法被语言提供(Aho–Kerninghan–Weinberger 1988; Bentley 1988)。例如,一个记录单词在文本中出现次数的程序的核心可以被写为 table[word] = table[word] + 1 而无需搜索词语的列表。 (然而,按字母顺序排列的报告需要一些实实在在的工作,因为 Lua 表中的索引是任意排序的。)
表可以以多种方式来创建。最简单的方法对应于普通数组:
t = @(100)
这样的表达式会生成一个新的的空表。表的尺寸(在上述例子中是100)是可选的,并且可以给初始表的大小一个提示。Lua 中的表可以根据需要进行动态扩展无论初始大小是多大。因此,对 t[200] 和 t["day"] 的引用都是完全有效的。
有两种创建表的语法 : 一个用于列表(@[]),一个用于记录(@{})。举例来说,很容易通过提供表的的元素来创建它,如
t = @["red", "green", "blue", 3]
这个代码与下面的代码等价
t = @()
t[1] = "red"
t[2] = "green"
t[3] = "blue"
t[4] = 3
此外,可以在创建列表或记录时提供用户自定义函数,如
t = @colors["red", "green", "blue", "yellow"]
t = @employee{name="john smith", age=34}
Thus, the code for the employee record is equivalent to:
在这里,colors 和 employee 都会在创建表后被自动调用。这样的函数可以被用来检查字段值,创建默认字段,或用于任何其他的有副作用的操作。因此,employee 操作记录的代码等价于:
t = @()
t.name = "john smith"
t.age = 34
employee(t)
需要注意的是,虽然 Lua 中没有类型声明,但是由于它有表创建后自动调用函数的功能,使 Lua 有了用户可控的类型构造函数。这种不常见的结构是一个非常强大的功能,在使用 Lua 进行声明式编程时可以这样如此表示。
-------------------
API
-------------------
实现 Lua 的 C 函数库中有一些使 Lua 和宿主进行交互的 API(大约有30个这样的函数)。这些函数使 Lua 作为嵌入式语言,可以处理下列任务:执行包含在一个文件或字符串中的 Lua 代码; 转换 C 和 Lua 中的值;读写的全局变量中的 Lua 对象;调用Lua 函数;注册可在 Lua 中调用的 C 函数,包括错误处理程序。一个简单的 Lua 解释可以写成如下:
#include "lua.h"
int main(void)
{
char s[1000];
while (gets(s))
lua_dostring(s);
return 0;
}
这个简单的解释器可以增加用C语言编写的特定领域函数,并可以使用函数 lua_register 注册给 Lua 使用。扩展函数遵循特定协议来接收和返回 Lua 中的值。
-------------------
预定义的函数和库
-------------------
Lua 的一组预定义函数虽少但功能强大。他们中大多数提供的功能让语言有一定程度的自反性。这些功能不能通过语言的其它部分模拟也不能通过标准的 API 模拟。预定义函数能处理以下任务:执行包含在一个文件或字符串中的 Lua 模块;遍历一个表的所有字段;枚举所有的全局变量;类型查询和转换。
库,在另一方面,提供了一种通过标准 API 实现的有用的程序。因此,它们并非语言必须的部分,并且作为单独的 C 模块被提供,它可以根据需要被连接到应用程序。目前,有字符串处理库,数学函数库,输入输出库。
-------------------
持久化
-------------------
枚举函数可以用来实现 Lua 全局环境的持久化,它可以写 Lua 代码,在执行时恢复所有全局变量的值。我们现在展示一些方法来存储和恢复 Lua 中的值,用 Lua 写成的文本文件作为存储媒介。用这种办法保存的值也可以很方便的恢复回来。
保存一个键值对,用下面的代码就可以了:
function store(name, value)
write(name .. '=')
write_value(value)
end
在这里,“..”是字符串连接操作,write 是用来输出的库函数。函数 write_value 根据 value 的类型输出一个合适的格式,value 的类型可以利用预先定义的函数 type 获得:
function write_value(value)
local t = type(value)
if t = 'nil' then write('nil')
elseif t = 'number' then write(value)
elseif t = 'string' then write('"' .. value .. '"')
end
end
存储表有点复杂。首先,write_value 中再加上
elseif t = 'table' then write_record(value)
假设表被用作记录(即没有循环引用,所有下标均为标识符),表的值可以用表的构造函数写成:
function write_record(t)
local i, v = next(t, nil) -- "next" enumerates the fields of t
write('@{') -- starts constructor
while i do
store(i,v)
i, v = next(t, i)
if i then write(', ') end
end
write('}') -- closes constructor
end
(未完待续)