粗浅地看,Lua 的 nil
很容易被等同于「无」。如下面这段代码:
function r_nil()
return nil
end
function r()
return
end
a = r_nil()
b = r()
print(a .. ", " .. b) --> nil, nil
尽管函数 r_nil()
和 r()
的返回语句分别带有和不带有 nil
,接受它们返回值的变量 a
和 b
的值都是 nil
。另一个例子是 nil
对 table 的作用。
tab_v = { attr1 = 1, attr2 = 2 }
for k,v in pairs(tab_v) do
print(k .. ", " .. v)
end --> attr1, 1
--> attr2, 2
tab_v.attr1 = nil
for k,v in pairs(tab_v) do
print(k .. ", " .. v)
end --> attr2, 2
将 table 的一个 field 赋值为 nil
不仅仅改变其值,而是让这个 field 本身消失了 (这个例子中是 field attr1
)。
分析 nil
的实际含义可以从 Lua 的另一个比较特殊的概念 —— list 入手。List 的特殊性在于它不是 first-class 类型。「First-class」是动态语言中常被提及的概念。编程语言有越多的构成元素符合 first-class 标准,其概念模型就越一致、越简单。Lua 的基本数据类型 (包括 nil
) 和函数都符合 first-class。满足 first-class 标准通常有四个要求:
- 可以被赋值给变量;
- 可以作为参数;
- 可以作为返回值;
- 可以作为数据结构的构成部分。( 注意
nil
并不完全符合这个要求,但是可以通过某个 field 的缺失来表示nil
。)
在《Programming in Lua, 2ed》的第 5.1 节提到 list 只能用于四种情形:
These lists appear in four constructions in Lua: multiple assignments, arguments to function calls, table constructors, and return statements.
List 有两种具体的表现形式,一种是用逗号分割的一组表达式,表示一个具体长度的 list;另一种是三个点构成的省略号 (...
),表示其长度和内容不定。第二种表示方式不能用在 multiple assignments 的等号左方,也不能创建新 list,只能从函数的形式参数列表中获得。由此可以看出,list 不符合 first-class 标准:
- 它的部分内容可以赋给几个变量,但本身不能作为整体赋给变量;
- 它是参数列表的全部或一部分,但不是任何参数 (注意两者的区别);
- 它不能作为数据结构的构成部分。注意,「
...
」不能作为 closure 的 upvalue。用 first-class function 存储 list 的方式行不通。
作为非 first-class 类型,list 无法被生命周期较长的数据结构存储。短期的完整传递 list 的内容只能利用函数调用/返回的方式:
function f_outer(...) -- important: f_out() must accept
-- "...".
end
f_outer(f_inner())
-- pass the list returned by f_inner() to
-- f_outer() as the latter's argument list
f_outer(1, 2, f_inner())
-- pass f_outer() a new list, which is "1, 2" appended
-- by f_inner()'s returned list
function f_caller(...)
f_callee(...) -- pass argument list of f_caller()
-- to f_callee()
return f() -- pass list returned by f() to one
-- level up
end
另外还有一些反例:
function f_caller(...)
a, b = ... -- not pass a list, "a, b" is
-- a different list of two elements
-- obtained by adjusting the "...",
-- and this list is very short-live,
-- existing in this line only
tab = {...} -- not pass a list, tab won't
-- have fields for nils in the "..."
local function test(a, b)
end
test(...) -- not pass a list, test()'s
-- argument list accepts only the first
-- two items of "..."
for i in ... do -- the "for" uses only the first
-- three elements in "..."
-- (two accepted by for internally,
-- and one received by i)
end
end
List 会成为 Lua 中为数不多的非 first-class 类型是因为它实际代表了 stack 上的一段数据。一般只有动态分配的数据能作为 first-class 类型,操纵 stack 上的数据则只能通过函数调用的参数和返回值等有限的方式进行 (这也是因为 stack 在一定程度上代表了程序的 continuation)。不过在其它语言中,stack 的内容并没有被抽象为类似 list 这样可以被操作 (尽管不能像 first-class 类型那样自由地操作) 的概念。因为 Lua 提供了多返回值,鼓励可变参数以及参数/返回值和 table 的互相转化,特别是它著名的 C 接口就以 stack 为中心来设计,所以它有了独特的 list 概念来操作 stack。
如果在 Lua 中一定要将 list 和 first-class 混用怎么办呢?比如说,一个函数返回的 list 通常还是要存储在变量中,或者应用在某个表达式中。这是上面的反例代码中已经提及的机制 —— adjustment。Adjustment 并不是真的传递一个 list 的内容,而是用一个 list 的内容构建另一个新的 list。当新 list 的长度小于原 list,多余的值被丢弃,当新 list 长度大于原 list,就用 nil
补齐。
Lua 的 nil
担当了三种角色:
- 一般的数据类型,通常标志某种特殊情况 (应用或算法本身的特殊情况,而非语言的特殊情况)。
- Table field 的删除器。
- List adjustment 的补全值。
Lua 的 nil
不代表「无」,反而恰恰起到了「有」的作用。在应用 adjustment 的情况下,我们往往用新 list 末尾的 nil
来判断原 list 的「无」。这个做法有一个缺陷:无法辩别原 list 末尾本来就确实含有的 nil
。如果需要区别对待 list 结束和 list 本身含有 nil
这两种情况,既可以自行编写 C 代码来检测 stack,也可以使用 Lua 现成的 API select()
。回到第一个例子,稍加修改就可以区别两种情况:
function r_nil()
return nil
end
function r()
return
end
a = select("#", r_nil())
b = select("#", r())
print(a .. ", " .. b) --> 1, 0
下面是精确区别 list 结束的一个实际例子 —— 关于 stack 的递归终止条件。若希望一个函数对它的 argument list 中的每个参数执行 op()
操作:
function map_list(op, ...)
if select("#", ...) == 0 then
return
else
local a = ...
return op(a), map_list(op,
sub_list(...))
end
end
函数 sub_list()
返回的 list 是其接受的 argument list 去掉第一个元素。这个函数的实现如下 (如果用 C 语言来实现会更简单)。如果 op()
允许接受 nil
并且在此情况下返回有意义的值,或者 map_list()
接受的 list 在中间含有 nil
,那么 map_list()
的递归终止条件就必须基于 select()
而不可以基于对 nil
的判断。
function sub_list(...)
local list_start
function list_start(start, ...)
if start > select("#", ...) then
return
else
return select(start, ...),
list_start(start + 1, ...)
end
end
return list_start(2, ...)
end
List 是 Lua 中最不符合 first-class 的数据类型。但由于其不能作为变量但可以被函数的返回值构建的特性,List 反而可能是 Lua 中最纯粹的 functional programming 元素。放弃 table 而完全用 list 来编写 Lua 程序也许是把 Lua 转化为一种 FP 语言最简单的手段。
发表评论