道可叨

Free Will

Programming in Lua 笔记

最近花了两天读完了《Programming in Lua》。本书不但教授如何使用 Lua 语言,还顺带解释了为什么 Lua 会设计成现在这样子,很有价值。

Lua 的特点: 小快灵

  • 小: 整个 Lua 包括标准库只有 2 万行代码,解释器大小 200 KB。占用的内存也很小
  • 快: 经过手工调优基于寄存器的虚拟机,加上增量式的标志清扫垃圾收集器。Lua 可算是运行速度最快的脚本语言了。另一方面,灵活的语法和数据结构也大大加快了开发速度
  • 灵: 使用方式灵活多样。可做为脚本语言直接使用;也可嵌入宿主程序中做脚本引擎 (如魔兽世界);还可直接调用 C 编写的扩展

Lua 的缺点: 少活简

  • 少: 标准库的功能太少。自限于标准 C 的框框中,导致很多基本功能缺失,例如正则表达式,系统线程,目录操作等
  • 活: 灵活的语法和数据结构固然能加快开发速度,但往往也使人容易犯下不着边际的错误。非常考验团队的编码纪律。特别是变量使用前不用定义,变量默认全局范围,类型自动协变这些设定
  • 简: Lua 的哲学之一是只提供机制,而不提供设施。导致很多用法是靠着惯例和潜规则来支撑的。对普通应用来说是简约,但对稍复杂的就显得简陋了

感想

  • Lua 太执着于标准 C 了,这种偏执让其保有极端的可移植性与可扩展性。例如 Lua 对库搜索路径的处理用巧妙的方式有意避开了文件路径,仅仅因为标准 C 中没有文件路径这个概念。自困于标准 C 直接导致了 Lua 的 os 库不能使用 POSIX API,几乎毫无用处。但也正是这个决定,让扩展非常方便,第三方很容易就写出 POSIX 的库供 Lua 使用。权衡得失,这个坚持还是相当大胆有效的

  • 由于正则表达式的实现过于庞大 (相比 Lua 其它部分来说),Lua 放弃了对它的支持,转而实现自己的字符串模式匹配方式。这是一个败笔。功能上的不同倒在其次,最重要的是大家要重新学习一套新的匹配规则。原有的经验知识浪费了。

  • 表 (table) 是 Lua 中唯一的数据结构,这也是从“小”来考虑的。带来的结果就是“活”与“简”的缺点。功能上来说,table 顶替 array, stack, map 等数据结构完全没有问题,性能上由于实现的巧妙性,也能接受。可问题在于 table 太灵活了,我们之所以使用某类数据结构,有时正是看中了它的限制。限制有利于开发者的沟通交流协同一致。极端的例子就是把表当数组使用时下标要从 1 开始,这个规矩居然只是惯例而非强制,太考验开发人员的素质了

  • 对于用 end 做结束符我有天生的抵触,Ruby 也是一例。Lua 的语法太过随意。末位的分号可以省略; 代码可以没有任何分割符挤在一行里; table 元素的分割符可用分号也可用逗号; 单参数函数调用可以省略括号 ... 也只有脚本语言能用这么灵活的语法。这方面我很喜欢 Python 的设定,语法严谨简单,没有这么多的例外

  • Lua 中有两个语法设定是亮点:

    • table 中末位元素允许可选的分割符
    • 多行字面字符串和多行注释可以自选括号样式

    前者方便了机器,后者方便了人。Json 规范如果当初规定末位元素可以有可选分割符的话,那 json 的模板生成代码就不用单独对最后的元素做特殊处理了

  • 变量不经定义就能使用,这实在是最大的败笔

  • 命令行参数列表的设定很有意思,可以通过负数下标来往前找 Lua 的启动参数

  • Lua 是弱类型语言,字符串和数字之间可以隐式转换。这是个错误,好处没有坏处一堆。书的作者对此也持负面看法

  • 与 Python 一样,变量只是名字,绑定的值由运行时管理

  • 只有 falsenil 算是假,其它的都是真,包括空表和空字符串。用惯 Python 的人会感到意外

  • 数字只有 double 类型。很漂亮的简化

  • 字符串可存任意数据,包括 \0 。象 Python 一样 Lua 的字符串也是不可变的

  • 不明白为什么要加入特殊的 # 号来取长度。加一个 len 函数不就行了吗

  • a.name() 不同于 a:name()a.name 也不是 a[name]

  • table 用做 array 时,可能会有洞 (hole),所以用 # 获取元素数量是不准的。因为下标可以跳着定义,所以用 table.maxn 算数量也是不准的。那用什么准 ? 得看你怎么用 table 了,这就是灵活的代价!写的时候,运用之妙存乎一心。等到维护代码的时候,就该骂娘了

  • Lua 对函数式编程支持的很好。函数是一级值语义,可内部嵌套定义,可绑定 lexical scoping 变量,支持尾递归优化。这比 Python 半生不熟的 lambda 要好太多了

  • 把乘方操作符 ^ 和取余操作符 % 的操作数都扩展到了小数,有很多有趣实用的应用

  • 虽然字符串和数字之间能隐式转换,但在比较相等时,字符串和数字是绝对不等的。Lua 总算保住了最后的理性

  • 基于逻辑操作符的短路机制,有 (x > y) and x or y 类似 C 中三元操作符 :? 的惯例,这点类似 Python 的 (x>y) and x or y

  • 定义局部变量要加 local 限定符。如果不加默认是全局变量。这是鼓励使用全局变量啊,失败!

  • 基于块的局部变量作用域。这点比 Python 只有全局,函数级,和 nonlocal 三种要细致好用

  • switch 是必须的吗? Lua 和 Python 都不使用 switch 而用 if elif 代替了。编译型语言用 switch 还可以方便编译器做优化,脚本语言确实没必要了

  • foriterator 结合,简化遍历语法。几种语言都有这种搞法 C++, Java, Python, C#

  • 如果只有一个参数,函数调用可以省略 () 。这种为方便偷懒而定的例外,很难认同

  • 比 Python 更灵活的是,Lua 不检查入参个数是否匹配。检查参数的担子落在了函数调用方和函数实现者身上

  • 象 Python 一样,Lua 函数可以返回多个值。不同的是,在多个返回值的取法上,Lua 有一套复杂规则,Python 只有一个规则。高下立判

  • Lua 的 iterator 协议中包含了不变状态和控制变量,增强了性能。Lua 可以比作脚本语言中的 C,非常贴近虚拟机

  • 象其他脚本语言一样,Lua 也可以在脚本中直接执行脚本字符串。那怎么控制安全性呢? Lua 又提供了 environment 机制让你实现 sandbox。记得几年前市面上 Python 的虚拟主机非常少,一个重要的原因是 Python 的功能太强大,又没有成熟的 sandbox 机制来控制,很难限制用户

  • 异常机制允许指定是哪层的问题,这个设定很新颖

  • 因为 ANSI C 标准没有线程,Lua 用非对称的协程来补缺。这比 Python 半调子的 generator 要好很多。当然,单个 Lua 没法利用多核优势

  • metatablemetamethod 是个因繁就简的方案。再一次体现了 Lua 通过把机制直接暴露给用户来达到小快灵的设计理念

  • 每个函数都可以设置自己的私有环境,这应该是针对 Lua 变量不用定义,默认全局范围语义的一个修正。早知如此,何必当初!

  • Lua 的对象模型与 javascript 一样,是基于 prototype 而非 class 的,非常灵活。要模拟 class 对象模型可以有多种方案。如果是编译型语言,基于 prototype 的对象模型实用吗 ?

  • 与 C 交互的接口使用栈来进行,简化了 C 接口模型。不过写起程序来稍微费点事。好在可以用第三方库来简化这些工作。

  • Lua API 总是需要显示传入一个 state 来指明是哪一个栈环境,而没有依赖任何全局变量,这使得 Lua 的解释器并发无碍

  • 在 5.1 版的 API 中,内存分配策略也可以定制。这里 API 的设计非常合理,包含了一个分配函数和一个用户数据指针。Lua 也提供了缺省的内存分配器

  • 关于垃圾收集器常遇到的 finalize, weak pointer,控制 gc 避免性能波动等问题,Lua 一个都没少。习惯了 C++ RAII 还真是觉得 GC 这套太麻烦了

总结

Lua 是一门局限性很大的语言,由于其小快灵的特点,在游戏脚本,编写扩展等领域是非常好的选择。可一旦程序上规模就会捉襟见肘。其极端的灵活性和简陋的设施,并不适合大团队协同开发。这年头语言层出不穷,新语言要想得到市场认可,四平八稳的设计思路是很难出头的。Lua 很多近乎偏质的设计决定,正是它得以流行的关键。

当然,Lua 还远谈不上是一门好语言。我一向认为,编程语言本身需肩负起教育与规劝的责任,应该用精巧的限制和有力的规则来避免码农犯愚蠢的错误。相反,把过多的自由交给无知的键盘,只会带给世界更多的垃圾代码。