函数

函数是 Lua 中,语句和表达式抽象的主要机制。函数既可以执行特定任务(在其他语言中有时称为 过程,procedure,或 子程序,subroutine),也可以计算并返回值。在第一种情况下,我们会将函数调用用作语句,use a function call as a statement;在第二种情况下,我们将函数调用,用作表达式,use it as an expression:

> print(8*9, 9/8)
72      1.125
> a = math.sin(3) + math.cos(10)
> print(os.date())
11/03/23 14:12:05

两种情况下,用括号括起来的参数列表,都表示了这种函数调用;如果调用没有参数,我们仍必须写下一个空列表,(),来表示函数调用。这条规则有一个特例:在函数只有一个参数,且该参数是字面字符串,或表构造器时,表示函数调用的括号,则是可选的:

> print "Hello World"       <-->    print("Hello World")
Hello World
> dofile 'lib.lua'          <-->    dofile("lib.lua")
> function f(a) ; end
> f{x=10, y=20}             <-->    f({x=10, y=20})
> type{}                    <-->    type({})
table

Lua 还为面向对象的调用,提供了一种特殊的语法,即冒号操作符。像 o:foo(x) 这样的表达式,就调用了对象 o 中的方法 foo。在第 21 章 面向对象编程 中,我们将更详细地讨论,这样的调用及面向对象编程。

Lua 程序可以使用在 Lua 及 C(或主机应用程序用到的任何其他语言)中,定义的函数。通常情况下,咱们使用 C 语言函数,是为了获得更好的性能,以及访问那些不易从 Lua 直接访问的设施(如操作系统设施)。例如,标准 Lua 库中的所有函数,都是用 C 语言编写的。不过,在调用函数时,Lua 中定义的函数,与 C 中定义的函数,并无区别。

正如我们在其他示例中看到的,Lua 中的函数定义,有着传统的语法,就像下面这样:

-- 将序列 'a' 的元素相加
function add (a)
    local sum = 0

    for i = 1, #a do
        sum = sum + a[i]
    end

    return sum
end

在这种语法中,函数定义包含了一个 名字,name(在示例中为 add)、一个 参数,parameters 列表,和一个 主体,body(即语句的列表)。参数的作用,跟使用函数调用中传递的参数值,所初始化的局部变量完全相同。

我们可以使用与其参数数量不同的参数,来调用某个函数。 Lua 通过丢弃额外的参数,以及为额外的参数提供一些 nil 值,来调整参数的数量。例如,请考虑下面这个函数:

function f (a, b) print(a, b) end

其有着以下行为:

> f()
nil     nil
> f(3)
3       nil
> f(3, 4)
3       4
> f(3, 4, 5)        --> 5 会被丢弃
3       4

尽管这样的行为可能导致编程错误,programming errors(通过最少的测试,即可轻松发现),但他也很有用,尤其是对于默认参数。例如,请考虑下面的对某个全局计数器递增的函数:

function incCount (n)
    n = n or 1
    globalCounter = globalCounter + n
end

该函数的默认参数为 1;调用 incCount()(不带参数)时,globalCounter 会递增 1。当我们调用 incCount() 时,Lua 会首先将参数 n,初始化为 nil;而 or 表达式的结果为第二个操作数,因此 Lua 会将 n 赋值为默认的 1

多个返回值

函数可以返回多个结果,这是 Lua 的一个非常规,但相当方便的特性。有几个 Lua 中预定义的函数,可以返回多个值。我们已经看到过函数 string.find,他可以在字符串中定位出某种模式。在找到模式时,该函数会返回两个索引:匹配开始处字符的索引,和匹配结束处字符的索引。多重赋值,a multiple assignment,允许程序获得这两个结果:

> s, e = string.find("Hello Lua users", "Lua")
> print(s, e)
7       9

(请记住,字符串第一个字符索引为 1。)

我们在 Lua 中编写的函数,也可以返回多个结果,方法是在 return 关键字后,列出所有结果。例如,查找某个序列中最大元素的函数,便可以返回最大值及其位置:

function maxium (a)
    local mi = 1            -- 最大值的索引
    local m = a[mi]         -- 最大值

    for i = 1, #a do
        if [ai] > m then
            mi = i; m = a[i]
        end
    end

    return m, mi
end

print(maxium({8, -1, 10, 23, 12, 5}))       --> 23      4

Lua 总是会根据调用的具体情况,调整函数结果的数量。当我们以语句形式调用函数时,Lua 会丢弃函数的所有结果。当我们将函数调用,作为表达式(例如加法的操作数)时,Lua 会只保留第一个结果。只有当调用是表达式列表中,最后一个(或唯一一个)表达式时,我们才能得到所有结果。这些列表会出现在 Lua 的四种结构中:

  • 多重赋值

  • 函数调用的参数

  • 表构造器

  • 以及 return 语句

为了说明所有这些情况,我们将在接下来的示例中,假设以下定义:

> function foo0 () end                      -- 不返回结果
> function foo1 ()  return 'a' end          -- 返回 1 个结果
> function foo2 ()  return 'a', 'b' end     -- 返回 2 个结果

在多重赋值中,作为最后(或唯一)表达式的函数调用,会产生与变量匹配所需的任意多个结果:

> x, y = foo2()
> print(x, y)
a       b
> x = foo2()                -- 这里返回的 'b' 会被丢弃
> print(x)
a
> x, y, z = 10, foo2()
> print(x, y, z)
10      a       b

在多重赋值中,如果函数的结果少于我们的所需,Lua 会为缺失的值生成一些 nil

> x,y = foo0()
> print(x, y)
nil     nil
> x,y = foo1()
> print(x, y)
a       nil
> x,y,z = foo2()
> print(x, y, z)
a       b       nil

请记住,只有当调用是列表中的最后(或唯一)的表达式时,才会出现多个结果。如果函数调用不是表达式列表中的最后一个元素,则总是只产生一个结果:

> x,y = foo2(), 20
> print(x, y)
a       20
> x,y = foo0(), 20, 30
> print(x, y)
nil     20

当函数调用是另一调用的最后一个(或唯一一个)参数时,第一个调用的所有结果,都将作为参数。我们已经在 print 中,看到了这种结构的例子。由于 print 可以接收可变数量的参数,因此语句 print(g()) 会打印出 g 返回的所有结果。

> print(foo0())             -- (没有结果)

> print(foo1())
a
> print(foo2())
a       b
> print(foo2(), 1)
a       1
> print(foo2() .. 'x')      -- (请参阅下文)
ax

当对 foo2 的调用,出现在某个表达式中时,Lua 会将结果数调整为一个;因此,在最后一行中,连接操作只会使用第一个结果 "a"

在我们写下 f(g()),且 f 有着固定参数数目时,Lua 会将 g 的结果数,调整为 f 的参数数目。这并非偶然,这与多重赋值中发生的行为,完全相同。

表构造器,也会收集调用的所有结果,而不做任何调整:

> t = {foo0()}                              -- t = {}(一个空表)
> for k, v in pairs(t) do print(k, v) end
> t = {foo1()}                              -- t = {'a'}
> for k, v in pairs(t) do print(k, v) end
1       a
> t = {foo2()}                              -- t = {'a', 'b'}
> for k, v in pairs(t) do print(k, v) end
1       a
2       b

与往常一样,只有当调用是列表中的最后一个(或唯一一个)表达式时,才会出现这种行为;在任何其他位置的调用,都只会产生一个结果:

> t = {foo0(), foo2(), 4}                   -- t = {nil, 'a', 4}
> for k, v in pairs(t) do print(k, v) end
2       a
3       4

注意:这里 t[1] 没有打印出来。

最后,return f() 这样的语句,会返回 f 所返回的所有值:

> function foo (i)
>> if i == 0 then return foo0()
>> elseif i == 1 then return foo1()
>> elseif i == 2 then return foo2()
>> end
>> end
>
> print(foo(1))
a
> print(foo(2))
a       b
> print(foo(0))

> print(foo(3))     -- (没有结果值)

通过用一对额外的括号,将调用括起来,咱们就可以强制其只返回一个结果:

> print((foo0()))
nil
> print((foo1()))
a
> print((foo2()))
a

请注意,return 语句不需要在返回值周围,加上括号;那里所加上的任何一对括号,都会算作额外的一对括号。因此,return (f(x)) 这样的语句,总是会返回单个值,而无论 f 返回多少个值。有时这正是我们想要的,有时却不是。

可变函数

Variadic Functions

Lua 中的函数可以是 可变的, variadic,这说的是,函数可以接受可变数量的参数。例如,我们已经以一个、两个或更多参数,调用了 print。虽然 print 是在 C 语言中定义的,但我们也可以在 Lua 中,定义可变函数。

举个简单的例子,下面的函数,返回所有参数的和:

function add (...)
    local sum = 0

    for _, v in ipairs{...} do
        sum = sum + v
    end

    return sum
end

print(add(0, 1, 3, 5, 7, 11))       --> 27

参数列表中的三个点(...),表示函数是可变的。当我们调用这个函数时,Lua 会在内部,收集所有参数;我们称这些收集到的参数,为函数的 额外参数,extra arguments。函数访问其额外参数时,会再次用到这三个点,现在则是作为表达式了。在我们的示例中,表达式 {...} 的结果,是一个包含了所有已收集参数的列表。然后,函数遍历该列表,以累加其中的元素。

我们将这种三点表达式,称为 可变参数表达式,vararg expression。其行为类似于多重返回函数,会返回当前函数的所有额外参数。例如,命令 print(...),便会打印该函数的所有额外参数。同样,下面这条命令,将以前两个可选参数的值(如果没有可选参数,则为 nil),创建出两个局部变量:

local a, b = ...

注意:上面语法的完整示例:

function add (...)
    local sum = 0
    local a, b = ...

    for _, v in ipairs{...} do
        sum = sum + v
    end

    return sum, a+b
end

add(0, 1, 3, 5, 7, 11, 13)      --> 输出为:40      1

其实,对于 Lua 在将

function foo (a, b, )

翻译到

function foo (...)
    local a, b, c = ...

时的一般的参数传递机制,我们可以加以模拟。

喜欢 Perl 参数传递机制的那些人,可能会喜欢第二种形式。

像下一个的这种函数,只会简单地返回所有参数:

function id (...) return ... end

这是一个多值标识函数。下一个函数的行为,则与另一个函数 foo 完全相同,只是在调用之前,会打印一条包含参数的信息:

function foo1 (...)
    print("calling foo:", ...)
    return foo(...)
end

这是跟踪特定函数调用的一种有用技巧。

我们来看另一个有用的例子。Lua 分别提供了格式化文本(string.format)和写入文本(io.write)两个函数。将这两个函数合并为一个可变函数,就非常简单:

function f_write (fmt, ...)
    return io.write(string.format(fmt, ...))
end

请注意,在三点之前,有一个固定参数 fmt。可变函数在可变部分之前,可以有任意数量的固定参数,fixed paramenters。Lua 会将靠前的参数,the first arguments,分配给这些参数,其余参数(如果有的话)作为额外参数。

为了遍历额外参数,函数可以使用表达式 {...},将他们全部收集到一个表中,就像我们在 add 的定义中所做的那样。然而,在额外参数可能是有效的一些 nil 的极少数情况下,用 {...} 创建的表,就可能不是正确的序列了。比如,在这样的表中,就无法检测原始参数中,是否有尾部的一些 nil。针对这种情况,Lua 提供了函数 table.pack注 1这个函数会接收任意数量的参数,并返回一个包含所有参数的新表(就像 {...}),但这个表还有一个额外的字段 "n",包含参数的总数。例如,下面的函数就使用了 table.pack,来测试是否其参数没有一个为 nil

注 1:这个函数时在 Lua 5.2 中引入的。

function nonils (...)
    local arg = table.pack(...)

    for i = 1, arg.n do
        if arg[i] == nil then return false end
    end

    return true
end


print(nonils(2,3,nil))      --> false
print(nonils(2,3))          --> true
print(nonils())             --> true
print(nonils(nil))          --> false

另一种遍历函数可变参数的方法,便是 select 函数。对 select 函数的调用,总是有着一个固定参数,即 选择器,selector,外加数量可变的额外参数。在选择器是个数 n 时,select 就会返回第 n 个参数后的所有参数;否则,选择器就应是字符串 "#",如此 select 会返回额外参数的总数目。

> select(1, "a", "b", "c")
a       b       c
> select(2, "a", "b", "c")
b       c
> select(3, "a", "b", "c")
c
> select("#", "a", "b", "c")
3

通常,我们在使用 select 时,会将其结果数调整为 1,因此我们可以将 select(n, ...),视为返回第 n 个额外参数。

> (select(1, "a", "b", "c"))
a
> (select(2, "a", "b", "c"))
b
> (select(3, "a", "b", "c"))
c

作为使用 select 的一个典型示例,下面是使用了 select 的我们之前的 add 函数:

function add (...)
    local sum = 0

    for i = 1, select("#", ...) do
        sum = sum + select(i, ...)
    end

    return sum
end

对于参数较少的情况,这个第二个版本的 add 更快,因为他避免了每次调用,都创建出一个新表。然而,对于参数较多的情况,多次调用有着多参数的 select 的开销,要比创建表的代价高,因此第一个版本成为更好的选择。(特别是,第二个版本的开销是二次方的,因为迭代次数,及每次迭代传递的参数数目,都会随着参数数目的增加而增加。)

关于函数 table.unpack

具有多重返回的一个特殊函数,是 table.unpack。他取得一个列表,并将列表中的所有元素,作为结果返回:

> table.unpack{10,20,30}
10      20      30
> a,b = table.unpack{10,20,30}      -- 30 会被丢弃
> print(a,b)
10      20

顾名思义,table.unpacktable.pack 相反。pack 将参数列表,转换为具体的 Lua 列表(表),而 unpack 则将具体的 Lua 列表(表),转换为返回列表,a return list,后者就可以作为参数列表,提供给另一个函数。

unpack 的一个重要用途,便是一种泛型调用机制中,in a generic call mechanism。泛型调用机制允许我们,以任意的参数,动态地调用任何函数。例如,在 ISO C 语言中,我们无法编写出泛型的调用。我们可以使用 stdarg.h,声明出接受可变参数的函数,也可以使用函数指针,调用某个可变函数。但是,我们无法调用某个参数数目可变的函数:在 C 语言中,咱们所编写的每个调用,都有固定的参数数目,每个参数都有固定的类型。在 Lua 中,如果我们想以数组 a 中的可变参数,调用可变函数 f,只需这样写即可:

f(table.unpack(a))

unpack 的调用,会返回 a 中所有的值,这些值会成为 f 的参数。例如,请考虑以下调用:

print(string.find("hello", "ll"))

我们可以用下面的代码,动态地建立一个等价调用:

f = string.find
a = {"hello", "ll"}

print(f(table.unpack(a)))

通常,table.unpack 会用到长度运算符,来确定出要返回多少个元素,因此他只适用于正确的序列。不过,如果需要,我们可以提供明确的限制:

> print(table.unpack({"Sun", "Mon", "Tue", "Wed"}, 2, 3))
Mon     Tue

虽然这个预定义的函数 unpack 是用 C 编写的,但我们也可以使用递归,using recursion,以 Lua 编写:

function _unpack (t, i, n)
    i = i or 1
    n = n or #t
    if i <= n then
        return t[i], _unpack(t, i + 1, n)
    end
end


_unpack({1, 3, 5, 7, 9})        --> 1       3       5       7       9
_unpack({1, 3, 5, 7, 9}, 2, 4)  --> 3       5       7

第一次使用单个参数调用时,参数 i1n 为序列的长度。然后,函数返回 t[1],接着返回 unpack(t, 2, n) 的所有结果,然后返回 t[2],接着返回 unpack(t, 3, n) 的所有结果,依此类推,直到返回 n 个元素后(递归)停止。

正确的尾部调用

Proper Tail Calls

Lua 中函数的另一个有趣特性,是 Lua 会消除尾部调用。(这意味着 Lua 具有 适当的尾部递归性,properly tail recursive,尽管这一概念并不直接涉及到递归;参见练习 6.6。)

所谓尾部调用,就是将 goto 伪装成调用。当某个函数作为其最后一个动作,调用另一函数时,就会发生尾部调用,这样他就没有其他事情可做了。例如,在下面的代码中,对 g 的调用就是尾调用:

function f (x) x = x + 1; return g(x) end

f 调用了 g 之后,就没有其他事情可做了。在这种情况下,程序不需要在被调用函数结束时,返回到调用函数。因此,在尾部调用后,程序无需在堆栈中,保留任何关于调用函数的信息。当 g 返回时,控制(CPU 底层部分)就可以直接返回到调用 f 的点。一些语言实现(如 Lua 解释器),利用了这一事实,在进行尾部调用时,实际上不会使用任何额外的栈空间。我们就说,这些实现消除了尾部调用。

由于尾部调用不会占用栈空间,因此程序可以构造无限次数的嵌套尾部调用。例如,我们可以将任意数字作为参数,调用下面的函数:

function foo (n)
    if n > 0 then return foo(n - 1) end
end

他永远不会溢出堆栈。

关于尾部调用消除,tail-call elimination,的一个微妙之处,便是什么是尾部调用。一些貌似显而易见的尾部调用,却不满足调用后没有其他事情可做的标准。例如,在下面的代码中,对 g 的调用,便不属于尾部调用:

function f (x) g(x) end

此示例中的问题,是在调用 g 之后,f 在返回之前,仍必须丢弃 g 的任何结果。同样,以下所有调用,均不符合标准:

return g(x) + 1     -- 必须完成这个加法
return x or g(x)    -- 必须调整到 1 个结果
return (g(x))       -- 必须调整到 1 个结果

在 Lua 中,只有 return func(args) 形式的调用,才是尾调用。然而,func 及其参数,都可以是复杂的表达式,因为 Lua 在调用之前,会对他们进行计算。例如,下一调用就是尾调用:

return x[i].foo(x[j] + a*b, i + j)

练习

练习 6.1:请编写出接受一个数组,并打印其所有元素的函数。

练习 6.2:请编写一个接受任意数量的值,并返回除第一个值之外其余值的函数。

练习 6.3:请编写一个接受任意数量的值,并返回除最后一个值之外其余值的函数。

练习 6.4:请编写一个打乱给定列表的函数。要确保所有排列的概率相同。

练习 6.5:请编写一个接受一个数组,并打印数组中元素的所有组合的函数。 (提示:咱们可以使用递归公式,进行组合:C(n,m) = C(n-1, m-1) + C(n- 1, m)。要生成所有 C(n,m) 的、大小为 m 的组中的 n 个元素的组合,咱们首先要将第一个元素添加到结果中,然后生成剩余槽中,剩余元素的所有 C(n-1, m-1) 组合;然后咱们从结果中删除第一个元素,然后在生成空闲槽中剩余元素的所有 C(n - 1, m) 种组合,当 n 小于 m 时,就没有组合了;当 m 为零时,只有一种组合,就不会使用任何元素。 )

练习6.6:有时,具有正确尾部调用的语言,被称为 正确尾部递归,properly tail recursive,并认为该属性仅当我们具有递归调用,才相关。 (如果没有递归调用,程序的最大调用深度,将是静态固定的。)

请证明这个论点,在像 Lua 这样的动态语言中不成立:编写一个程序,执行无递归的无界调用链,performs an unbounded call chain without recurion。 (提示:请参阅名为 “编译” 的小节。)

Last change: 2023-11-05, commit: 0f18b90