Lua学习笔记(六):函数-续
Lua中的函数是带有词法定界(lexical scoping)的第一类值(first-class values)。第一类值指:在Lua中函数和其他值(数值、字符串)一样,函数可以被存放在变量中,也可以存放在表中,可以作为函数的参数,还可以作为函数的返回值。词法定界指:嵌套的函数可以访问他外部函数中的变量。这一特性给Lua提供了强大的编程能力。
Lua中关于函数稍微难以理解的是函数也可以没有名字,匿名的。当我们提到函数名(比如print),实际上是说一个指向函数的变量,像持有其他类型的变量一样:
1 a = {p = print}
2 a.p("Hello World") --> Hello World
3 print = math.sin -- `print' now refers to the sine function
4 a.p(print(1)) --> 0.841470
5 sin = a.p -- `sin' now refers to the print function
6 sin(10, 20) --> 10 20
既然函数是值,那么表达式也可以创建函数了,Lua中我们经常这样写:
1 function foo (x) return 2*x end --这实际上是Lua语法的特例,原本的函数:
2 foo = function (x) return 2*x end
函数定义实际上是一个赋值语句,将类型为function的变量赋给一个变量。我们使用function(x)..end来定义一个函数和使用{}创建一个表一样。
table标准库提供一个排序函数,接受一个表作为输入参数并排序表中的元素。这个函数必须能够对不同类型的值(字符串或者数值)按升序或者降序进行排序。Lua不是尽可能多地提供参数来满足这些情况的需要,而是接受一个排序函数作为参数(类似c++的函数对象),排序函数接受两个排序元素作为输入参数,并且返回两者的大小关系,例如:
network = {
{name = "grauna", IP = "210.26.30.34"},
{name = "arraial", IP = "210.26.30.23"},
{name = "lua", IP = "210.26.23.12"},
{name = "derain", IP = "210.26.23.20"},
}
--如果我们想通过表的name域排序
table.sort(network, function (a,b)
return (a.name > b.name)
end)
以其他函数作为函数的函数在Lua中被称作高级函数(higher-order function),如上面的sort。在Lua中,高级函数与普通函数没有区别,他们只是把"作为参数的函数"当作第一类值(first-class value)处理而已。
下面给出一个绘图函数的例子:
1 function eraseTerminal()
2 io.write("\27[2J")
3 end
4
5 -- writes an '*' at column 'x' , 'row y'
6 function mark (x,y)
7 io.write(string.format("\27[%d;%dH*", y, x))
8 end
9
10 -- Terminal size
11 TermSize = {w = 80, h = 24}
12
13 -- plot a function
14 -- (assume that domain and image are in the range [-1,1])
15 function plot (f)
16 eraseTerminal()
17 for i=1,TermSize.w do
18 local x = (i/TermSize.w)*2 - 1
19 local y = (f(x) + 1)/2 * TermSize.h
20 mark(i, y)
21 end
22 io.read() -- wait before spoiling the screen
23 end
24
25 --要想让这个例子正确运行,你必须调整你的终端类型和代码中的控制符一致
26 plot(function (x) return math.sin(x*2*math.pi) end) --将在屏幕上输出一个正玄曲线
1、闭包
当一个函数内部嵌套另一个函数定义时,内部的函数体可以访问外部的函数的局部变量,这种特征我们称作词法定界。虽然这看起来很清楚,事实并非如此,词法定界加上第一类函数在编程语言里是一个功能强大的概念,很少语言提供这种支持。
1 function newCounter()
2 local i = 0
3 return function() -- anonymous function
4 i = i + 1
5 return i
6 end
7 end
8
9 c1 = newCounter()
10 print(c1()) --> 1
11 print(c1()) --> 2
匿名函数使用upvalue i保存他的计数,当我们调用匿名函数的时候i已经超出了作用范围,因为创建i的函数newCounter已经返回了,然而Lua用闭包的思想正确处理了这种情况。简单的说,闭包是一个函数以及他的的upvalues。如果我们再次调用newCounter,将创建一个新的局部变量i,因此我们得到了一个作用在新的变量i上的新闭包。
1 c2 = newCounter()
2 print(c2()) --> 1
3 print(c1()) --> 3
4 print(c2()) --> 2
c1、c2是建立在同一函数上,但作用在同一个局部变量的不同实例上的两个不同的闭包。
技术上来讲,闭包指值而不是指函数,函数仅仅是闭包的一个原型声明;尽管如此,在不会导致混淆的情况下我们继续使用术语函数代替闭包。
闭包在完全不同的上下文中也很有用途的。因为函数被存储在普通的变量内我们可以很方便的重定义或者预定义函数。通常当你需要原始函数有一个新的实现时可以重定义函数。例如你可以重定义sin使其接受一个度数而不是弧度作为参数:
1 oldSin = math.sin
2 math.sin = function (x)
3 return oldSin(x*math.pi/180)
4 end
5
6 --更清楚的方式
7 do
8 local oldSin = math.sin
9 local k = math.pi/180
10 math.sin = function (x)
11 return oldSin(x*k)
12 end
13 end
这样我们把原始版本放在一个局部变量内,访问sin的唯一方式是通过新版本的函数。
利用同样的特征我们可以创建一个安全的环境(也称作沙箱,和java里的沙箱一样),当我们运行一段不信任的代码(比如我们运行网络服务器上获取的代码)时安全的环境是需要的,比如我们可以使用闭包重定义io库的open函数来限制程序打开的文件。
1 do
2 local oldOpen = io.open
3 io.open = function (filename, mode)
4 if access_OK(filename, mode) then
5 return oldOpen(filename, mode)
6 else
7 return nil, "access denied"
8 end
9 end
10 end
2、非全局函数
Lua中函数可以作为全局变量也可以作为局部变量,我们已经看到一些例子:函数作为table的域(大部分Lua标准库使用这种机制来实现的比如io.read、math.sin)。这种情况下,必须注意函数和表语法:
--1、表和函数放在一起
Lib = {}
Lib.foo = function (x,y) return x + y end
Lib.goo = function (x,y) return x - y end
--2、使用表构造函数
Lib = {
foo = function (x,y) return x + y end,
goo = function (x,y) return x - y end
}
--3、Lua提供另一种语法方式
Lib = {}
function Lib.foo (x,y)
return x + y
end
function Lib.goo (x,y)
return x - y
end
当我们将函数保存在一个局部变量内时,我们得到一个局部函数,也就是说局部函数像局部变量一样在一定范围内有效。这种定义在包中非常有用的:因为Lua把chunk当作函数处理,在chunk内可以声明局部函数(仅仅在chunk内可见),词法定界保证了包内的其他函数可以调用此函数。下面是声明局部函数的两种方式:
--1、方式一
local f = function (...)
...
end
local g = function (...)
...
f() -- external local `f' is visible here
...
end
--2、方式二
local function f (...)
...
end
有一点需要注意的是在声明递归局部函数的方式:
1 local fact = function (n)
2 if n == 0 then
3 return 1
4 else
5 return n*fact(n-1) -- buggy
6 end
7 end
上面这种方式导致Lua编译时遇到fact(n-1)并不知道他是局部函数fact,Lua会去查找是否有这样的全局函数fact。为了解决这个问题我们必须在定义函数以前先声明:
1 local fact
2
3 fact = function (n)
4 if n == 0 then
5 return 1
6 else
7 return n*fact(n-1)
8 end
9 end
这样在fact内部fact(n-1)调用是一个局部函数调用,运行时fact就可以获取正确的值了。
但是Lua扩展了他的语法使得可以在直接递归函数定义时使用两种方式都可以。
在定义非直接递归局部函数时要先声明然后定义才可以:
1 local f, g -- `forward' declarations
2
3 function g ()
4 ... f() ...
5 end
6
7 function f ()
8 ... g() ...
9 end
3、正确的尾调用(Proper Tail Calls)
Lua中函数的另一个有趣的特征是可以正确的处理尾调用(proper tail recursion, 一些书使用术语"尾递归",虽然并未涉及到递归的概念)。
尾调用是一种类似在函数结尾的goto调用,当函数最后一个动作是调用另外一个函数时,我们称这种调用为尾调用。如:
1 function f(x)
2 return g(x)
3 end
4
5 --g的调用是尾调用
例子中f调用g后不会在做任何事情,这种情况下当被调用函数g结束时程序不需要返回到调用者f;所以尾调用之后程序不需要在栈中保留关于调用者的任何信息。一些编译器比如Lua解释器利用这种特性在处理尾调用时不使用额外的栈,我们称这种语言支持正确的尾调用。
由于尾调用不需要使用栈空间,那么尾调用递归的层次可以无限制的。例如下面调用不论n为何值不会导致栈溢出。
1 function foo (n)
2 if n > 0 then return foo(n - 1) end
3 end
4
5 --需要注意的是:必须明确什么是尾调用,比如下面就不属于尾调用:
6 function f (x)
7 g(x)
8 return
9 end
10 --下面也不是尾调用
11 return g(x) + 1 -- must do the addition
12 return x or g(x) -- must adjust to 1 result
13 return (g(x)) -- must adjust to 1 result
14
15 --Lua中类似return g(...)这种格式的调用是尾调用。
16 --但是g和g的参数都可以是复杂表达式,因为Lua会在调用之前计算表达式的值。如下面的调用也是尾调用:
17 return x[i].foo(x[j] + a*b, i + j)
可以将尾调用理解成一种goto,在状态机的编程领域尾调用时非常有用的。状态机的应用要求函数记住每一个状态,改变状态只需要goto一个特定的函数。