说明:
这个文档是 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
(未完待续)
html