第06章 深入函数

上章介绍了一些函数的基本内容后,本章主要涉及三个方面的内容:闭包(closure), 非全局函数(non-global function), 合理的尾调用(proper tail calls)。

闭包 closure

词法域(lexical scoping)指一个函数可以嵌套在另一个函数中,内部的函数可以访问外部的函数的变量。

Lua 的函数的第一类值。Lua 支持 FP。函数和所有其他值一样都是匿名的,程序如此使用函数:通过函数名持有某函数的变量。

函数的定义举例:

function foo(x)
    return 2*x
end

这其实不是“能反映本质的写法”,只是一种语法糖而已,“能反映 Lua 本质的写法”如下所示:

foo = function(x)
    return 2*x
end

由此可见,函数即一条赋值语句,它创建了一种类型为“函数”的值,并将这个值赋个一个变量。

函数是可以作为参数的,如对 table 内元素排序:

names = {"Peter", "Paul", "Mary"}
grades = {Mary=10, Paul=7, Peter=8}
table.sort(names, function (n1, n2) return grades[n1] > grades[n2] end)
for i, v in pairs(names) do
    print(v)
end

像 sort 这样接受一个函数作为实参的,称为“高阶函数”。

再举一个例子:返回一个函数的导函数:

function derivative2 (f, delta)
    delta = delta or 1e-5
    return    function (x)
                  return (f(x+delta) - f(x))/delta
              end
end
print(math.cos(3.5), derivative2(math.sin)(3.5))

输出结果是:

-0.9364566872908    -0.93645493336458

值非常接近

TODO, 用 Java 和 JavaScript 如何实现这个功能?尤其是用 Java?貌似 JavaScript 中还可以类似实现,Java 是不太可能了。只有支持函数式编程的才能做到吧!

一个函数的形参在函数内部是局部变量,但如果一个在高阶函数内部函数,高阶函数的形参在这个内部函数里,既不是全局变量,也不是局部变量,lua 称之为 non-local varial, 历史上也称为 upvalue(这个称法在其他语言里也还有的)

为什么说函数只是一种特殊的 closure?只是没有 upvalue 的 closure 而已。

closure 的又一个示例:

function newCounter()
    local i = 0
    return    function()
                i = i + 1
                return i
            end
end
c1 = newCounter()
c2 = newCounter()
print(c1()) --> 1
print(c1()) --> 2
print(c2()) --> 1
print(c1()) --> 3

closure 的又一个示例:

do
    local oldSin = math.sin
    local k = math.pi / 180
    math.sin = function (x)
        return oldSin(x*k)
    end
end

非全局函数 non-global function

把函数定义在 table 中,函数成为 table 中的元素。大部分 lua 库把函数存储在 table 中,如 io.read, math.sin (TODO: 查查库的源码看看)

Lua 将每个程序块(thunk)作为一个函数来处理,在一个程序块中声明的函数就是局部函数,这些局部函数值在该程序块中可见。

阶乘如果写成

local fact = function (n)
    if n == 0 then
        return 1
    else
        return n*fact(n-1)
    end
end
print(fact(5))

将出现运行错误,因为到 else 那布里,fact 还没有定义完成,这句实际上调用了一个全局的 fact,并非自身,应该改写为:

local fact
fact = function (n)
    if n == 0 then
        return 1
    else
        return n*fact(n-1)
    end
end
print(fact(5))

对于

local fuction foo <参数表> <函数体> end, Lua

会将其展开为:

local foo
foo = function ......

所以,

local function fact(n)
    if n == 0 then
        return 1
    else
        return n*fact(n-1)
    end
end
print(fact(5))

也会正常运行并输出120,注意这个技巧对间接递归调用的函数无效。不妨所有的地方都是用明确的前向声明,如

local fact
fact = function .......

这样子。

附注:貌似 jQuery 也有类似的思想的体现,都是提供一个统一的前缀(说法不精确TODO)。

尾调用 tail call

当一个函数是另一个函数的最后一个动作时,就是“尾调用”。尾调用在有的书中也称为尾递归。但有个要求,最后一个动作必须仅为调用一个函数,而不能再有其他的。return x + g(x), return (f(x)) 等都不是尾调用,因为调用完其他函数之后,还要做运算,如果不是尾调用,但调用了别的函数,那么会只返回这个调用的第一个值。如下面的程序:

f1 = function ()
    return 1, 2, 3
end
f2 = function ()
    return f1()
end
f3 = function ()
    return (f1())
end
f4 = function()
    return 3*f1()
end
a, b, c = f1()
print(a, b, c)
a, b, c = f2()
print(a, b, c)
a, b, c = f3()
print(a, b, c)
a, b, c = f4()
print(a, b, c)

输入是:

1    2    3
1    2    3
1    nil    nil
3    nil    nil

f2 是尾调用,所以和 f1 的输出结果是一样的,f3 和 f4 都有计算,只返回了计算后的第一个值。