概述
翻译自原文档 Microlight,删去了 Array 与实验性的那些部分。
基于 C89 标准的Lua 标准库被刻意设计得很小巧。它作为高级开发的基础,所以 Lua 开发者喜欢在项目中收集一些有用的小的函数。
Microlight 是对于 library golf1 的一项尝试。在本项目中,我们的想法就是捕捉这些函数,放到一个库里面,再给它们写文档,使得用起来比自己写起来更容易。
这个库作为极轻量的 Penlight,有接近 20 个模块和上百个函数。
在 Lua 中,除了内核之外的东西都可以有自己的选择,下面的函数不想成为权威的选择。它们大多出现在由 Jay Carlson 创始的 Lua Mailing 列表中,然后被我和 Dirk Laurie 所实现。
字符串
Lua 没有内置将表转换成字符串形式的函数,这使得刚开始使用 Lua 交互模式的用户很沮丧。Microlight 提供了 tstring。注意,在全局重定义 tostring 不是一个好做法。这个技巧只是为了让命令行下的实验更尽人意。
> require 'ml'.import()
> tostring = tstring
> = {10,20,name='joe'}
{10,20,name="joe"}
Lua 的字符串函数异常强大,但是其中却缺少了一些项目中很常用的函数。Lua 提供了 table.concat 合并一个表,但没有提供 table.split 分割字符串。
> = split('hello dolly')
{"hello","dolly"}
> = split('one,two',',')
{"one","two"}
其中的第二个参数是一个匹配模式,默认匹配空格。
虽然在 Lua 下进行字符串插值并不困难,但是没有直接的函数可以完成这项工作。所以 Microlight 提供了 ml.expand。
> = expand("hello $you, from $me",{you='dolly',me='joe'})
hello dolly, from joe
expend 函数也可以理解 $var 的另一个表达方式 ${var},而且可以接受一个函数作为第二个参数。
Lua 的字符串函数使用了匹配模式,一个正则表达式的强大的子集。其中定义了魔法字符,比如 .,$ 之类的。在某些场合,使用包含这些字符的字符串之前需要进行转义,escape 函数就可以创建这样一个字符串字面量。
> = ('woo%'):gsub(escape('%'),'hoo')
"woohoo" 1
> = split("1.2.3",escape("."))
{"1","2","3"}
文件与路径
虽然 access 在大多数平台上都得到了支持,但是它不是标准的一部分(这也是为什么它在 Windows 上被拼写为 _access)。所以判断一个文件是否存在,你得尝试打开它。
function ml.exists (filename)
local f = io.open(filename)
if not f then
return nil
else
f:close()
return filename
end
end
返回值不是简单的 true 或 false,它返回的是原文件名,所以我们可以在一群候选文件中找出一个真正存在的文件。
> = exists 'README' or exists 'readme.txt' or exists 'readme.md'
"readme.md"
Lua 长于切割文本,所以通用的方法就是读完个不是很大的文件然后在字符串上进行操作。读取的工作可以交给 readfile。如下的代码返回了以二进制打开的文件的前 128 个字节。
> txt = readfile('readme.md',true):sub(1,128)
注意我指的是字节,而不是字符,因为字符串编码可能是任意字节长度的。
如果 readfile 在打开或读取文件时发生了错误,它会返回 nil 和错误消息,这也是 io.open 与其它很多 Lua 函数的错误处理模式。在常规问题上抛出异常不是一个好方法。
使用 splitpath 和 splitext 可以拆分一个路径。
> = splitpath(path)
"/path/to/dogs" "bonzo.txt"
> = splitext(path)
"/path/to/dogs/bonzo" ".txt"
> = splitpath 'frodo.txt'
"" "frodo.txt"
> = splitpath '/usr/'
"/usr" ""
> = splitext '/usr/bin/lua'
"/usr/bin/lua" ""
这些函数返回了两个字符串,其中一个可能是空字符串(而不是 nil)。在 Windows 上,接受反斜杠与正斜杠,在 Unix 上,只接受正斜杠。
插入与扩展
大多数 Microlight 的函数与 Lua 的表有关。虽然表可能既是数组又是映射,我们通常把它们区分开来。现在,我们会用数组与映射来指代表。
update 把键值对添加到一个映射中,extend 把一个数组追加到另一个数组上。它们是两个互补的,向表塞入多个项的操作。
> a = {one=1,two=2}
> update(a,{three=3,four=4})
> = a
{one=1,four=4,three=3,two=2}
> t = {10,20,30}
> extend(t,{40,50})
> = t
{10,20,30,40,50}
从 1.1 版本开始,这两个函数都接受任意个数的参数。
想要把表「铺平」,只要将其 unpack 然后再使用 extend 即可。
> pair = {{1,2},{3,4}}
> = extend({},unpack(pair))
{1,2,3,4}
简明地,extend 以一个可索引可写的对象作为参数,从 #O+1 开始写入元素,其中 #O 代表原数组的长度,而且原数组必须是没有空缺的。类似的,另外的几个数组也必须是可索引的,但不一定是可写的。这些参数通常但不一定是表,你可以利用 extend 总是从 1 写到 #t 这个行为,把第一个数组写成这个形式。
> obj = setmetatable({},{ __newindex = function(t,k,v) print(v) end })
> extend(obj,{1,2,3})
1
2
3
把一堆值插入数组的指定位置,你可以使用 insertvalues,它与 table.insert 类似,除了它可以接受一个数组作为插入值。如果你打算覆盖原数组的值,只要把第四个参数置为 true。
> t = {10,20,30,40,50}
> insertvalues(t,2,{11,12})
> = t
{10,11,12,20,30,40,50}
> insertvalues(t,3,{2,3},true)
> = t
{10,11,2,3,30,40,50}
注意,原表在进行这些操作之后已经改变了。
update 与 extend 类似,然而它可以处理一个映射,原始值在操作后可能被覆盖。
> t = {}
> update(t,{one=1},{ein=1},{one='ONE'})
> = t
{one="ONE",ein=1}
截取与操作
扩展的相反操作就是从表中截取出一部分项目。
sub 函数,与 string.sub 类似,它和 Python 中的列表切片操作室等效的。
> numbers = {10,20,30,40,50}
> = sub(numbers,1,1)
{10}
> = sub(numbers,2)
{20,30,40,50}
> = sub(numbers,1,-2)
{10,20,30,40}
indexby 可以在表内索引一系列值。
> = indexby(numbers,{1,4})
{10,40}
> = indexby({one=1,two=2,three=3},{'three','two'})
{[3,2}
imap 可以创建一个新数组,并把函数作用在所有元素上。
> words = {'one','two','three'}
> = imap(string.upper,words)
{"ONE","TWO","THREE"}
> s = {'10','x','20'}
> ns = imap(tonumber,s)
> = ns
{10,false,20}
imap 总是返回一个和原数组相同大小的数组,如果函数返回 nil,它会用 false 填补空缺。
还有一个常用函数 indexof 可以在数组中进行线性查找并返回下标,如果不成功,则返回 nil。
> = indexof(numbers,20)
2
> = indexof(numbers,234)
nil
这个函数有一个可选的判断谓词作为第三个参数。
函数 ifilter 可以选出原数组中的所有合适的值。
> = ifilter(numbers,tonumber)
{"10","20"}
imap 和 ifilter 结合产生了 imapfilter,它相当于一个删除无用项的 imap 函数。
> = imapfilter(tonumber,{'one',1,'f',23,2})
{1,23,2}
collect 函数可以从迭代器中收集值,形成一个数组。collectuntil 可以用谓词决定终止条件,collectn 简单地收集指定数目个值。
> s = 'my dog ate your homework'
> words = collect(s:gmatch '%a+')
> = words
{"my","dog","ate","your","homework"}
> R = function() return math.random() end
> = collectn(3,R)
{0.0012512588885159,0.56358531449324,0.19330423902097}
> lines = collectuntil(4,io.lines())
one
two
three
four
> = lines
{"one","two","three","four"}
下面是一个简单的命令行排序程序。
require 'ml'.import()
lines = collect(io.lines())
table.sort(lines)
print(table.concat(lines,'\n'))
最后,removerange 从数组中删除一段值,它和 sub 有相同的参数,但它是原地工作的,意味着原数组会被修改。
集合与映射
在数据量大时,indexof 不是一个好选择,因为它进行的是线性查找,在哈希表中查找肯定会更快。invert 可以把数组值变成一个表中的键。
> m = invert(numbers)
> = m
{[20]=2,[10]=1,[40]=4,[30]=3,[50]=5}
> = m[20]
2
> = m[30]
3
> = m[25]
nil
> m = invert(words)
> = m
{one=1,three=3,two=2}
于是我们从数组得到了一个表方便地搜索值,也可以方便地去重。在这里,我们不关心值,只关心键。
> = issubset(m,{one=true,two=true})
true
makemap 可以从两个数组中构建出一个表来。
> = makemap({'a','b','c'},{1,2,3})
{a=1,c=3,b=2}
高阶函数
函数是 Lua 中的一等公民,所以函数可以操作函数,这种函数叫做高阶函数。
一个对象可执行,当且仅当它是一个函数,或者它的元表中有 __call 项。callable 函数可以检查这个性质。
组合两个函数通常很有用。
> printf = compose(io.write,string.format)
> printf("the answer is %d\n",42)
the answer is 42
bind1 和 bind2 生成函数的一个特化,它们的参数个数会比以前少一个。bind1 把函数的第一个参数绑定为一个特定的值,这可以用来把成员函数变成普通函数。在 Lua 中,obj:f(...) 是 obj.f(obj, ...) 的缩写。然而这还不够,Lua 没有提供成员函数的隐式绑定,bind1 恰好可以完成这个工作。
> ewrite = bind1(io.stderr.write,io.stderr)
> ewrite 'hello\n'
简单的日志就是输出内种后再进行换行。
> log = bind2(ewrite,'\n')
> log 'hello'
hello
注意 sub(t,1) 就是一个简单的数组拷贝函数。
> copy = bind2(sub,1)
> t = {1,2,3}
> = copy(t)
{1,2,3}
可以这样断言字符串是不是空的。
> blank = bind2(string.match,'^%s*$')
> = blank ''
""
> = blank ' '
" "
> = blank 'oy vey'
nil
返回值不是简单的 true 或 false,事实上 Lua 在后期才有了 false 的概念。在这里 nil 代表了 false,任何字符串都代表 true。
可以用这个模式生成一群判断函数,比如用 %x+ 判断十六进制数,用 %u+ 判断大写字符串。你还可以继续玩这个捆绑游戏(大雾)。
> matcher = bind1(bind2,string.match)
> hex = matcher '^%x+$'
这样的判断函数在 ifind 和 ifilter 中很有用。
Lua 函数返回多个值很常见,有时候你只要第二个返回值,take2 就有用了。
> p = lfs.currentdir()
> = p
"C:\\Users\\steve\\lua\\Microlight"
> = splitpath(p)
"C:\\Users\\steve\\lua" "Microlight"
> basename = take2(splitpath)
> = basename(p)
"Microlight"
> extension = take2(splitext)
> = extension 'bonzo.dog'
".dog"
函数 map2fun 和 fun2map 可以实现表和函数之间的互相转换。这在 API 要求一个函数,但我们只有表的时候很有用。
> obj = objects:find ('X.name=Y','Alfred')
{name='Afred',age=23}
> by_name = function(name) return objects:find('X.name=Y',name) end
> lookup = fun2map(by_name)
> = lookup.Alfred
{name='Alfred',age=23}
如果你很聪明,或者是一个 S,那就可以写这样的匿名函数。
by_name = bind1('X:find("X.name==Y",Y)',objects)
类
Lua 和 Javascript 有两个共同点,对象都是扩展的表(有语法糖 t.key == t['key']),而且没有内置的类机制。这在一开始会让人很不爽,也会使人造出很多种不兼容的轮子。
Animal = ml.class()
Animal.sound = '?'
function Animal:_init(name)
self.name = name
end
function Animal:speak()
return self._class.sound..' I am '..self.name
end
Cat = class(Animal)
Cat.sound = 'meow'
felix = Cat('felix')
assert(felix:speak() == 'meow I am felix')
assert(felix._base == Animal)
assert(Cat.classof(felix))
assert(Animal.classof(felix))
类可以创建一个表,表包含了一些方法。如果类有基类,基类也会被拷贝到表内。类表是每个实例的元表,如果 obj.key 在实例本身中没有被找到,Lua 会在它的类表中尝试寻找。这样更高效,因为每个实例不用携带函数的拷贝。
这个类表是可执行的,每次执行它都会返回一个新对象,如果有 _init 方法的话,它就会在此时作为构造函数被调用。
每个实例都有一个 _class 项指向它的类表,也可以用 classof 得到一个实例的类表。
字符串函数
split(s, re, n)
分割字符串
s: 输入字符串re: 一个 Lua 字符串匹配模式,默认为%s+,即以空白字符分割n: 可选的,最大分割数目
返回一个包含字符串的数组
escape(s)
转义字符串中的所有魔法字符 ^$()%.[]*+-?
s: 输入字符串
返回一个转义后的字符串
expand(s, subst)
填充一个字符串,其中包含形容 ${var} 或 $var 的模板。注意用于填充的值必须是一个字符串或者一个数
s: 输入字符串subst: 一个表或者一个函数,用于查询填充值
返回填充后的字符串
readfile(filename, is_bin)
以字符串的形式读取一个文件
filename: 文件路径is_bin: 是否以二进制的形式打开文件
返回文件内容,或者 nil 表示出错
writefile(filename, str, is_bin)
将字符串写入一个文件
filename: 文件路径str: 写入的字符串is_bin: 是否以二进制的形式打开文件
返回 true,或者 nil 表示出错
文件系统函数
exists(filename)
返回文件是否存在
filename: 文件路径
返回原来的文件路径,或者 nil 表示不存在,下面是一个例子:
file = exists 'readme' or exists 'readme.txt' or exists 'readme.md'
splitpath(P)
将路径划分为文件夹部分和文件名部分,如果文件夹部分不存在,则第一个返回值会是空字符串。可以在 Windows 上处理正斜杠和反斜杠两种路径
P: 文件路径
返回两个值,文件夹部分和文件名部分
splitext(P)
将路径划分为文件名部分和扩展名部分,如果扩展名部分不存在,则第二个返回值会是空字符串
P: 文件路径
返回两个值,文件名部分和扩展名部分
扩展的表处理函数
这里的「数组」是指形似数组的表,针对数组的函数只会对1..#t这部分的表进行操作,而且针对了这个特性进行了特殊优化。
tstring(t, how)
将 Lua 值的字符串表示返回。作用于表时,可以检测环的存在,而且返回结果可以是良好格式化的。
t: 待转换的值how: 可选的,一个表包含了键值spacing和indent,或者仅仅一个字符串代表indent
返回一个字符串
collect(…)
从一个迭代器中收集一串值
...: 一个迭代器
返回一个数组。下面是一个例子,其中keys(t)代表表中的所有键:
collect(pairs(t)) is the same as keys(t)
collectuntil(f, …)
从一个迭代器中收集一串值,直到谓词返回 true
f: 谓词函数...一个迭代器
返回一个数组
collectn(n, …)
从一个迭代器中收集一串值,直到收集完 n 个值
n: 收集的值的个数...: 一个迭代器
返回一个数组
collect2nd(…)
从一个迭代器中收集一串值,这里收集的总是返回的第二个值。如果第二个值不存在,会被跳过而不是被收集
...: 一个迭代器
返回一个数组,下面是一个例子:
collect2nd(pairs{ one=1, two=2 }) is {1, 2} or {2, 1}
mapextend(dest, j, nilv, f, t, …)
通过一个函数将一个表扩展到另一个表
dest: 目标表j: 目标表中的下标起始值nilv: 如果函数返回nil,默认的替换值f: 一个函数t: 原表...: 函数的额外参数
返回被扩展的表,即 dest
imap(f, t, …)
把一个函数作用在一个数组上,其中返回 nil 的部分会被设置为 false
f: 一个函数t: 原数组...: 函数的额外参数
返回一个新数组,这个函数与下面的函数功能相同:
mapextend(t,1,false,f,t,...)
transform(f, t, …)
把一个函数原地作用到一个数组上
f: 一个函数t: 原数组...: 函数的额外参数
返回变换后的数组
imap2(f, t1, t2, …)
把一个函数作用在两个数组上,这个函数接受两个值。返回数组的长度是两个数组中短的那个的长度`
f: 一个函数t1: 第一个数组t2: 第二个数组...: 函数的额外参数
返回一个新数组
imapfilter(f, t, …)
类似 imap,但是nil值会被删除
f: 一个函数t: 原数组...: 函数的额外参数
返回一个新数组
ifilter(t, pred, …)
筛选数组,留下谓词返回 true 的那部分
t: 原数组pred: 谓词函数...: 函数的额外参数
返回一个新数组
ifind(t, pred, …)
用谓词在数组中找到第一个匹配值。
t: 原数组pred: 谓词函数...: 函数的额外参数
返回一个值,下面是一个例子
ifind({{1,2},{4,5}},'X[1]==Y',4) is {4,5}
indexof(t, value, cmp)
返回数组中某个值的下标
t: 原数组value: 指定值cmp: 谓词函数,默认为X==Y
sub(t, i1, i2)
返回一个数组的切片,类似于string.sub
t: 原数组i1: 起始下标,默认为 1i2: 终止下标,默认为#t
返回切片,包括起点与终点
removerange(tbl, start, finish)
删除数组中的一段值
tbl: 原数组start: 起始下标finish: 终止下标
insertvalues(dest, index, src, overwrite)
将 src 中的元素拷贝到 dest,起始下标为 index,默认会移动 dest中的元素来腾出空间
dest: 目标数组index: 复制到的起始下标src: 原数组overwrite: 是否直接覆盖而不是位移
extend(t, …)
通过其他数组来扩展一个数组
t: 被扩展的数组...: 其他数组
返回被扩展的数组
indexby(t, keys)
在表内选出指定的键值对,返回值所形成的数组
t: 一个表keys: 键所形成的数组
返回值所形成的数组,下面是两个例子:
indexby({one=1,two=2},{'one','three'}) is {1}
indexby({10,20,30,40},{2,4}) is {20,40}
range(x1, x2, d)
创建一个表,表示从 x1 到 x2,步长为 d 的数列
x1: 起始值x2: 终值d: 步长,默认为1
返回一个数组,下面是两个例子
range(2,10) is {2,3,4,5,6,7,8,9,10}
range(5) is {1,2,3,4,5}
update(t, …)
将多个表合并至第一个表,对于重复的键值,来自第一个表的会被舍弃
t: 被更新的表...: 被合并的表
返回被更新后的表
makemap(t, tv)
用键数组与值数组构建一个表
t: 键数组tv: 值数组
返回构建出来的表,下面是一个例子:
makemap({'power','glory'},{20,30}) is {power=20,glory=30}
invert(t)
用一个数组构建一个集合,其中的键是数组中的值
t: 原数组
返回一个表,下面是一个例子:
invert({'one','two'}) is {one=1,two=2}
keys(t)
构建以表中所有键形成的数组
t: 原表
返回一个数组
issubset(t, other)
判断是否为子集,以键为判断标准
t: 一个集合other: 另一个集合
返回 other 是否为 t 的子集
containkeys(t, other)
这是 issubset 的一个别名
count(t)
求表的大小
t: 一个表
返回表的大小
equalkeys(t, other)
判断两个表是否有完全相同的键
t: 一个表other: 另一个表
返回是否有完全相同的键
函数式
throw(f)
抛出原函数的异常
f: 一个函数,异常时会返回nil,err
返回一个等效的可以抛出错误的函数
bind1(f, v)
把 v 绑定到函数 f 上的第一个参数
f: 一个至少接受一个参数的函数v: 一个参数
返回一个函数,接受的参数比原函数少一个
bind1(f, v)
把 v 绑定到函数 f 上的第二个参数
f: 一个至少接受一个参数的函数v: 一个参数
返回一个函数,接受的参数比原函数少一个
compose(f1, f2)
合并两个函数
f1: 外层函数f2: 内层函数
返回合并后的函数,相当于f1(f2(...))
take2(f)
得到原函数的第二个返回值
f: 原函数,至少返回两个参数
返回一个函数,它会返回原函数的第二个返回值,下面是一个例子:
take2(splitpath) is basename
callable(obj)
判断一个对象是否可被调用
obj: 被判断的对象
返回时否可被调用
map2fun(t)
把一张表转换成一个函数的形式
t: 一张表或者是另外的可索引对象
返回一个函数
fun2map(f)
把一个函数转换成一张表的形式
t: 一个函数,只有一个参数
返回一张表
function_arg(f)
把东西变成可执行的函数,这个函数支持把 string lambda 转换为函数。所谓 string lambda ,就是一个直接返回的,以 X,Y,Z 代表其三个参数的一个字符串。
f: 一个可执行体或者一个 string lambda
返回一个函数。如果 f 不是可执行的或者不是一个合法的 string lambda ,就会抛出异常,下面是一个例子:
function_arg('X+Y')(1,2) == 3
memoize(func)
记忆化一个函数
func: 一个至少接受一个参数的函数
返回一个函数,这个函数只会记忆化其第一个参数
类
class(base)
以 base 为父类创建一个类
base: 可选的,父类
返回一个表代表类