分享

Programming in Lua(四)- Nil 和 List ? 技术奇异点

 quasiceo 2014-01-05

Programming in Lua(四)- Nil 和 List

粗浅地看,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,接受它们返回值的变量 ab 的值都是 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 语言最简单的手段。

这篇文章发布于 2012年12月22日,星期六,22:15,归类于 Lua, 开源, 软件开发。 您可以跟踪这篇文章的评论通过 RSS 2.0 feed。 您可以留下评论,或者从您的站点trackback

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多