第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 都有计算,只返回了计算后的第一个值。