Chapter 6: More About Functions
函数可以被存在变量和表里面,可以被作为参数传递,可以被作为函数的返回值。
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
函数的另一种写法
function foo (x) return 2*x end -- 实际上这种写法是一个语法糖
foo = function (x) return 2*x end -- 本质写法
表达式function(x) body end 是一个函数构造器,就像表构造器{} 一样。
匿名函数的应用:将表的元素按字典序排序
(类似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"},
}
table.sort(network, function (a,b) return (a.name < b.name) end)
for i in pairs(network) do print(network[i].name) end
高级应用
导数的非正式定义:函数f 在点x 上的导数是(f(x + d) - f(x)) / d 当d 趋于无穷小时的值
function derivative (f, delta)
delta = delta or 1e-4
return function (x)
return (f(x + delta) - f(x))/delta
end
end
c = derivative(math.sin)
print(math.cos(10), c(10))
--> -0.83907152907645 -0.83904432662041
6.1 Closures
包含在另一个函数里的函数
包含在另一个函数里的函数可以访问包含它的那个函数的局部变量,这个特性称为lexical scoping
一个表以另一个表作为条件排序
names = {"Peter", "Paul", "Mary"}
grades = {Mary = 10, Paul = 7, Peter = 8}
function sortByGrades(names, grades)
table.sort(names, function (n1, n2)
return grades[n1] > grades[n2] -- 分数高的在前
end)
end
sortByGrades(names,grades)
for i in pairs(names) do print(names[i]) end
匿名函数访问了局部变量,既sortByGrades 的参数
上面例子的匿名函数内,grades 既不是全局变量也不是局部变量,我们称为non-local variable.
记数器
function newCounter ()
local i = 0
return function () -- anonymous function
i = i + 1
return i
end
end
c1 = newCounter()
print(c1()) --> 1
print(c1()) --> 2
每调用一次newCounter() 都会创建一个新的non-local variable
c2 = newCounter() -- 记数器2 新值
print(c2()) --> 1
print(c1()) --> 3 -- 记数器1 还保留原来的值
print(c2()) --> 2
匿名函数里面既不是全局变量又不是局部变量的变量(非局部变量)会一直保留其值??
Closures 对回调很有用
function digitButton (digit)
return Button{ label = tostring(digit),
action = function ()
add_to_display(digit)
end
}
end
重定义函数
将math.sin 重定义成使用度,而不是弧度
do
local oldSin = math.sin
local k = math.pi/180
math.sin = function (x) -- 重定义math.sin
return oldSin(x*k) -- 这里的k 就是一个non-local variable
end
print (math.sin(90)) -- 这里的math.sin 用度
math.sin=oldSin
end
print (math.sin(90)) -- 这里的math.sin 用弧度
沙盒:将不安全版本函数做为私有变量存在闭包里面,外部不再可以访问
对于不安全的不信任代码,将它放进沙盒里面
do
local oldOpen = io.open
local access_OK = function (filename, mode)
<check access>
end
io.open = function (filename, mode)
if access_OK(filename, mode) then
return oldOpen(filename, mode)
else
return nil, "access denied"
end
end
end
io.open 的旧版本已经作为局部变量被放在do.end 里面,外部只能访问新版本。
6.2 Non-Global Functions
非全局函数
作为表中一个元素的函数我们已经见过了,如io.read, math.sin,其创建方法如下:
Lib = {}
Lib.foo = function (x,y) return x + y end
Lib.goo = function (x,y) return x - y end
或
Lib = {
foo = function (x,y) return x + y end,
goo = function (x,y) return x - y end
}
或
Lib = {}
function Lib.foo (x,y) return x + y end
function Lib.goo (x,y) return x - y end
当我们将函数存在局部变量里面,我们就获得了一个局部函数
定义一个局部函数
local f = function (<params>)
<body>
end
或使用语法糖式的写法
local function f (<params>)
<body>
end
递归局部函数的定义方法
local function fact (n)
if n == 0 then
return 1
else
return n*fact(n-1)
end
end
或
local fact
fact = function (n)
if n == 0 then
return 1
else
return n*fact(n-1)
end
end
实际上前面那个语法糖或展开成以下形式
local foo
foo = function (<params>) <body> end
错误的定义
local fact = function (n)
if n == 0 then
return 1
else
return n*fact(n-1) -- buggy
end
end
非显式递归函数的定义不能使用语法糖:
local f, g -- 'forward' declarations
function g ()
<some code> f() <some code>
end
function f ()
<some code> g() <some code>
end
6.3 Proper Tail Calls
如果函数的最后一个动作是调用另一个函数,这个调用就是尾部调用。
function f (x) return g(x) end
对函数g 的调用是一个尾部调用。
Lua 的一个有趣特性是tail-call elimination
尾部调用消除使得上面的例子中,函数g 不是返回f ,而是直接返回调用f 的那个点。这样就不必为f 在栈中保留任何返回信息。
永运不会发生栈溢出的函数
function foo (n)
if n > 0 then return foo(n - 1) end
end
下面这个函数不是尾部调用消除的:
function foo (n)
if n == 0 then return 1 elseif n > 0 then return n * foo(n - 1) end
end
foo(50000) -- 栈溢出
这些函数也不是尾部调用消除的:
function f (x) g(x) end
f 返回前要丢弃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
tail-call 就是 go to
将tail-call用于状态机
应用程序可以将每个状态用一个函数表示
改变一个状态就是从一个函数进入另一个函数。
考虑一个简单的maze game 游戏,maze 有许多房间,每一个房间可能有四个门:北、南、东,西。在每一步中,游戏者进入一个移动方向,如果这个方向上有门及与之相连的房间则进入相应的房间。否则,程序打印一条警告信息。游戏的目的是从一个初始房间移动到目标房间。
这个游戏是一个典型的状态机,当前房间就是一个状态。
我们可以为每一个房间实现一个与之相应的函数。我们使用tail-call 从一个房间移动到另一个房间。程序6.1 实现了有四个房间的小型maze 游戏。
游戏从调用room1 开始,进入起始房间:
room1()
如果没有尾部调用消除,游戏者的每一次移动都会创建一个新栈帧。经过大量移动后,就可能造成栈溢出。使用了尾部调用消除就不会有移动次数的限制,因为每次移动实际上只是执行了一个goto 进入另一个函数,这不同于常规函数调用。
程序6.1 A maze game:
function room1 ()
local move = io.read()
if move == "south" then
return room3()
elseif move == "east" then
return room2()
else
print("invalid move")
return room1() -- 无效的移动,留在当前房间(既再次进入当前房间)。
end
end
function room2 ()
local move = io.read()
if move == "south" then return room4()
elseif move == "west" then return room1()
else
print("invalid 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