Lua编程6章(V5.0)

来源:互联网 发布:阿卢浮漂淘宝官网 编辑:程序博客网 时间:2024/05/18 15:56
第六章 再论函数
Lua 中的函数是带有词法定界(lexical scoping)的第一类值(first-class values)。 
第一类值指:在 Lua 中函数和其他值(数值、字符串)一样,函数可以被存放在变
量中,也可以存放在表中,可以作为函数的参数,还可以作为函数的返回值。 
词法定界指:被嵌套的函数可以访问他外部函数中的变量。这一特性给 Lua 提供了
强大的编程能力。 
Lua 中关于函数稍微难以理解的是函数也可以没有名字,匿名的。当我们提到函数
名(比如 print),实际上是说一个指向函数的变量,像持有其他类型值的变量一样: 
a = {p = print}
a.p("Hello World") --Hello World
print = math.sin --`print` now refers to the sine function
a.p(print(1)) --0.841470
sin = a.p --`sin` now refers to the print function 
sin(10, 20) --10 20
既然函数是值,那么表达式也可以创建函数了,Lua 中我们经常这样写:
function foo (x) return 2*x end 
这实际上是利用 Lua 提供的“语法上的甜头”(syntactic sugar)的结果,下面是原
本的函数: 
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中被称作高级函数,高级函数在 Lua 中并没有特
权,只是 Lua 把函数当作第一类函数处理的一个简单的结果。
下面给出一个绘图函数的例子:
function eraseTerminal()
io.write("\27[2J")
end


-- wirte an `*' at column `x', row `y'
function mark(x, y)
io.write(string.format("\27[%d;%dH*", y, x)
end 


-- Terminal size
TermSize = {w = 80, h = 24}
-- plot a  function
-- (assume that domain and image are in the range [-1, 1])
function plot (f)
eraseTerminal()
for i = 1,TermSize.w, do
local x = (i/TermSize.w)*2 - 1
local y = (f(x) + 1)/2 * TermSize.h
mark (i, y)
end
io.read() -- wait before spoiling the screen
end


要想让这个例子正确的运行,你必须调整你的终端类型和代码中的控制符一致:
plot(function(x) return math.sin(x*2*math.pi)  end)
将在屏幕上输出一个正弦曲线


6.1闭包
当一个函数内部嵌套另一个函数定义时,内部的函数体可以访问外部的函数的局部
变量,这种特征我们称作词法定界。虽然这看起来很清楚,事实并非如此,词法定界加
上第一类函数在编程语言里是一个功能强大的概念,很少语言提供这种支持。 
下面看一个简单的例子,假定有一个学生姓名的列表和一个学生名和成绩对应的表;
现在想根据学生的成绩从高到低对学生进行排序,可以这样做: 
names = {"Peter", "Paul", "Mary"} 
grades = {Mary = 10, Paul = 7, Peter = 8} 
table.sort(names, function (n1, n2) 
  return grades[n1] > grades[n2]    -- compare the grades 
end) 


创建一个函数实现此功能
function sortbygrade (names, grades)
table.sort(names, function (n1, n2)
return grades[n1] > grades[n2]
end
end


例子包含在sortbygrade函数内部的sort中的匿名函数可以访问sortbygrade的参数grades,在匿名函数内部grades不是全局变量也不是局部变量,我们称作外部的局部变量(external local variable)或者upvalue.(upvalue意思
有些误导,然而在Lua中他的存在也有历史根源,还有他比起external local varible简短)
看下面代码


function newCounter()
local i = 0
return function()
i = i+1
return i
end
end


c1 = newCounter()
print(c1()) -->1
print(c2()) -->2


匿名函数使用upvalue i 保存它的计数,当我们调用匿名函数的时候i已经超出了作用范围,因为创建i的函数
newCounter已经返回了。然而Lua用闭包的思想正确处理了这种情况。简单的说闭包是一个函数加上它可以正确访问的upvalues。
如果我们再次调用newCounter,将创建一个新的局部变量i,因此得到了一个作用在新的变量i上的新闭包。
c2 = newCounter()
print(c2())
print(c1())
print(c2())
c1, c2是建立在同一个函数上,但作用在同一个局部变量的不同实例上的两个不同的闭包。
技术上来讲,闭包指值而不是指函数,函数仅仅是闭包的一个原型声明;尽管如此,
在不会导致混淆的情况下我们继续使用术语函数代指闭包。 
闭包在上下文环境中提供很有用的功能,如前面我们见到的可以作为高级函数(sort)
的参数;作为函数嵌套的函数(newCounter)。这一机制使得我们可以在 Lua 的函数世界
里组合出奇幻的编程技术。闭包也可用在回调函数中,比如在 GUI 环境中你需要创建一
系列 button,但用户按下 button 时回调函数被调用,可能不同的按钮被按下时需要处理
的任务有点区别。具体来讲,一个十进制计算器需要 10 个相似的按钮,每个按钮对应一
个数字,可以使用下面的函数创建他们: 


function digitButton (digit)
return Button{ label = digit,
action = function()
add_to_display(digit)
end
end


这个例子中我们假定 Button 是一个用来创建新按钮的工具, label 是按钮的标签,
action 是按钮被按下时调用的回调函数。(实际上是一个闭包,因为他访问 upvalue digit)。
digitButton 完成任务返回后,局部变量 digit 超出范围,回调函数仍然可以被调用并且可
以访问局部变量 digit。 


闭包在完全不同的上下文中也是很有用途的。因为函数被存储在普通的变量内我们
可以很方便的重定义或者预定义函数。通常当你需要原始函数有一个新的实现时可以重
定义函数。例如你可以重定义 sin 使其接受一个度数而不是弧度作为参数: 


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


更清楚的方式:


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


这样我们把原始版本放在一个局部变量内,访问 sin 的唯一方式是通过新版本的函
数。 


利用同样的特征我们可以创建一个安全的环境(也称作沙箱,和java里的沙箱一样)
当我们运行一段不信任的代码(比如我们运行网络服务器上获取的代码)时安全的环境
是需要的,比如我们可以使用闭包重定义 io 库的 open 函数来限制程序打开的文件。 
do
local oldOpen = io.open
io.open = function (filename, mode)
if access_OK(filename, mode) then
return oldOpen(filename, mode)
else
return nil, "access denied"
end
end
end


6.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 heref
end
2.方式二
local function f (...)
...
end
有一点需要注意的是声明递归局部函数的方式
local fact = function (n)
if n == 0 then 
return 1
else
return n * fact(n-1)-- buggy
end
end


上面这种方式导致Lua编译时遇到fact(n-1)并不知道它是局部函数 fact, Lua 会去查
找是否有这样的全局函数 fact。为了解决这个问题我们必须在定义函数以前先声明:


local fact


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


这样在fact内部fact(n-1)调用是一个局部函数调用,运行时fact就可以获取正确的值了。
但是Lua扩展了他的语法使得可以在直接递归函数定义时使用两种方式都可以。 
在定义非直接递归局部函数时要先声明然后定义才可以: 
local f, g
function g()
... f() ...
end


function f()
... g() ...
end


6.3正确的尾调用 (Proper Tail Calls)
Lua中函数的另一个有趣的特征是可以正确的处理尾调用(proper tail recursion,一
些书使用术语“尾递归”,虽然并未涉及到递归的概念)。 
尾调用是一种类似在函数结尾的 goto 调用,当函数最后一个动作是调用另外一个函
数时,我们称这种调用尾调用。例如: 
function f(x) 
  return g(x) 
end 
g 的调用是尾调用。
例子中 f 调用 g 后不会再做任何事情,这种情况下当被调用函数 g 结束时程序不需
要返回到调用者 f;所以尾调用之后程序不需要在栈中保留关于调用者的任何信息。一
些编译器比如 Lua 解释器利用这种特性在处理尾调用时不使用额外的栈,我们称这种语
言支持正确的尾调用。 
由于尾调用不需要使用栈空间,那么尾调用递归的层次可以无限制的。例如下面调
用不论 n 为何值不会导致栈溢出。 
function foo (n) 
  if n > 0 then return foo(n - 1) end 
end 
需要注意的是:必须明确什么是尾调用。 
一些调用者函数调用其他函数后也没有做其他的事情但不属于尾调用。比如: 
fucntion f (x)
g(x)
return 
end 


上面这个例子中 f 在调用 g 后,不得不丢弃 g 地返回值,所以不是尾调用,同样的
下面几个例子也不是尾调用: 


return g(x) + 1   -- must do the addition 
return x or g(x)   -- must adjust to 1 result ,必须调整到返回一个结果
return (g(x))    -- must adjust to 1 result ,必须调整到返回一个结果


Lua中类似return g(...)这种格式的调用是尾调用。但是 g 和 g 的参数都可以是复杂
表达式,因为 Lua 会在调用之前计算表达式的值。例如下面的调用是尾调用:
return x[i].foo(x[j] + a*b, i + j) 


可以将尾调用理解成一种 goto,在状态机的编程领域尾调用是非常有用的。状态机
的应用要求函数记住每一个状态,改变状态只需要 goto(or call)一个特定的函数。我们考
虑一个迷宫游戏作为例子:迷宫有很多个房间,每个房间有东西南北四个门,每一步输
入一个移动的方向,如果该方向存在即到达该方向对应的房间,否则程序打印警告信息。
目标是:从开始的房间到达目的房间。 


这个迷宫游戏是典型的状态机,每个当前的房间是一个状态。我们可以对每个房间
写一个函数实现这个迷宫游戏,我们使用尾调用从一个房间移动到另外一个房间。一个
四个房间的迷宫代码如下: 
function room1()
local move = io.read()
if move == "south" then
return room3()
elseif move == "east" then
return room2()
else
print("invalid move")
return room1 -- stay in the same room
end
end


function room2()
local move = io.read()
if move == "south" then
return room4()
elseif move == "west" then 
return room1()
else
print("ivalid move")
return room2()
end
end


function room3()
local move = io.read()
if move == "north" then 
return room1()
elseif move == "east" then 
return room4()
else
print("invalid move")
return room3()
end
end


function room4()
print("congratulations!")
end

我们可以调用 room1()开始这个游戏。 
如果没有正确的尾调用,每次移动都要创建一个栈,多次移动后可能导致栈溢出。
但正确的尾调用可以无限制的尾调用,因为每次尾调用只是一个 goto 到另外一个函数并

不是传统的函数调用。


原创粉丝点击