chapter6 深入了解函数

时间:2022-06-21 17:35:43

  Lua函数是具有特定词法域的第一类值,与其他传统类型的值(string and number)具有相同的权利。

它可以保存在变量和table中,也可以把它当参数传递,也可以作为返回值。

  在Lua中有个容易混淆的概念,函数与所有其它值一样都是匿名的,即他们没有名称。

当讨论一个函数名(print),实际上是讨论一个持有某函数的变量。

这与其他变量持有各种值一个道理,可以用多种方式来操作这些变量:

a  =  {p = print}
a.p(
"Hello World") -->Hello World
print = math.sin -->"print" 现在引用了sin函数
a.p(print(1)) -->0.841470
sin = a.p -->"sin" 现在引用了print函数
sin(10,20) -->10 20

通常我们这样定义一个函数:

function foo (x) return 2*x end

这种写法仅仅是下面方式的一种简写:

foo = function(x) return 2*x end

实际上,函数定义就是创建了一个function类型的值,并把它赋值给一个变量。

可以将上面的function(x) body end表达式叫做函数构造式,就像{}是一个table构造式一样。

将这种函数构造式的结果叫作匿名函数。一些场合会用到匿名函数:

table.sort可以接收不同的排序方式,升序、降序、数字顺序、字符排序......

table.sort没有这些方式的函数实现,需要通过传入order function参数。

order function接收两个l列表内元素为参数,当第一个元素需要排在第二个元素之前,返回真并有序返回这两个参数。比如有一个table:

network = {
{name
= "grauna", IP = "210.26.20.30"},
{name
= "arraial", IP = "210.26.45.33"},
{name
= "lua", IP = "210.45.34.56"},
{name
= "derain", IP = "210.26.23.76"},
}

如果想以name字段按照反向的字符顺序进行排序,则:

table.sort(network,function (a,b) return (a.name > b.name) end)
--有点不好理解,a.name > b.name是ture 然后function是要排序还是不进行排序。
--
但是从测试结果去了解,是要让table.name按照d,c,b,a进行排序
--
索性就把(a.name > b.name )当成想要实现的结果,这样好理解一点

把这样的sort函数称作高阶函数,然后用匿名函数来创建高阶函数需要的实参。

另一个用高阶函数的例子,一个函数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

对于特定函数f调用derivative(f)将(近似地)返回其导数:

c = derivative(math.sin)
print(math.cos(5.2),c(5.2))
--> 0.46851667130038 0.46856084325086
print(math.cos(10),c(10))
--> -0.83907152907645 -0.83904432662041

6.1 闭合函数

  把一个函数嵌入到另一个函数内,并且它具有访问闭合函数里的局部变量。

 有以下代码:

names  = {"Peter", "Paul" , "Mary"}
grades
= {Mary=10,Paul=7, Peter=8}
table.sort(name,function(n1,n2) return grades[n1] > grades [n2] end

现在,用一个函数去实现这个功能:

function sortbygrade (names,grades)
table.sort(names,function (n1,n2) return grades[n1] > grades[n2] end
end

匿名函数function可以访问grades变量,grades变量是闭合函数sortbygrade的局部变量。

对于匿名函数,grades既不是全局变量,也不是局部变量,我们称它为upvalue——上值。

Why is this point so interesting? Because functions are first-class values,
and therefore they can escape the original scope of their variables.

这是原文,没有理解透彻。待以后再细细品尝。

要对upvalue的深入了解,参考云风博客里的一篇译文:the_implementation_of_lua_50.pdf

 参考以下代码:的

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

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

 在这个代码中,匿名函数引用了一个非局部变量i去计数。

当在调用匿名函数时,newCounter函数已经返回,i变量超出作用范围。

 对此,Lua用了一个叫做closure的概念去处理这种情况。

closure是一个函数加上所有它需要访问的非局部变量。

当再重新调用newCounter,它会创建一个新的局部变量i,从而得到一个新的closure。

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

此时,c1和c2是两个不同的closure函数,各自有独立的局部变量i。

从技术层面讲,Lua中只存在closure,不存在function。

function本身仅仅是closure的原形。

closure是一个很有用的工具,比如在高阶函数sort中作为参数传参。

对创建其他函数的函数也很有用(比如沙盒函数)。

这种机制使Lua程序可以融合那些在函数式编程世界中久经考验的编程技术。

closure对于回调函数也很有用。

一个典型的例子:当你调用传统的GUI 工具创建按钮时。每当用户按下按钮,就会触发一个

回调函数。你希望的是不同的按钮按下时,只做一点轻微不同的操作。

比如一个 十进制的计算器,需要10个相似的按钮,每个数字对应一个。

function digiButton (digit)
return Button { label = tostring(digit),
action
= function()
add_to_display(digit)
  
end
}
end

该例子中,Button是一个创建按钮的函数,label是按钮标签,action是一个closur函数,每

次有按钮按下就会触发。即使在digitButton函数返回,digit变量超出范围。action任然可以

去访问这个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保存为一个私有变量,只有通过新版本的sin才可以访问到它。

可以使用该技术创建一个安全的运行环境,即所谓的sandbox——”沙盒”。

当运行一些不受信任的代码时会用到该功能。比如限制一个程序访问文件:

do 
local oldOpen = io.open
local access_OK = function (filename ,mode)
--check access authority
end
io.open = function(filename,mode)
if access_OK(filename,mode)
return oldOpen(filename,mode)
else
return nil,"access denied"
end
end
end

这样,只能通过新授权的版本才能访问原来的不受限的open函数。

对此Lua提供了一套元机制,可以根据特定的安全需要来创建一个安全的运行环境。

 6.2 非全局函数

  大多数的Lua库(io.read,math.sin)使用了这样的机制,把函数放在table字段里。

在Lua中创建这样的函数,只需要将函数语法与table语法结合起来即可。

 

Lib = {}
Lib.foo
= function (x,y) return x+y end
Lib.goo
= function (x,y) return x-y end
print(Lib.foo(2,3),Lib.goo(2,3)) -->5 -1

 

也可以这样写:

Lib = {
goo
= function (x,y) return x+y end,
foo
= 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()
<body>
end

local g = function()
f()
--f在这里可见
end

对于这种局部函数的定义,Lua支持的一种特殊的语法糖:

local  function f()
<body>
end

当Lua在展开局部函数定义的语法糖时:并不是使用基本函数定义的语法。

local function foo()   <body> end

---->展开后

local foo;foo = function() <body> end

 

在定义递归函数时,下面的代码是不能运行的:

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

原因是,当Lua编译fact (n-1)时,local fact 还没有定义。此时该表达式将会去调用全局的fact,而不是local fact。

解决办法:

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

------------------或者----------------------

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

 

 当函数运行时,fact已经有了确切的值,所以不会产生错误。

当间接使用递归时,你必须明确地使用前向声明。

local f , g
function g()
f()

end

function f()
g()
end

注意,别把第二个函数定义为local function f,否则 g 中调用的 f 处于未定义状态。

6.3 正确的尾调用

Lua函数的另一个特性,Lua支持尾调用消除。

当一个函数调用是另一个函数的最后一个动作,该调用就是一个尾调用。

有点类似于goto语句。

比如:

 

function f(x) return g(x) end

 

当g返回时,控制权直接返回到调用f的那个点上。

尾调用消除这个特点,使得在进行 “尾调用” 时不消耗任何栈空间。

所以一个程序可以有无数嵌套的“尾调用”

function foo(n)
if n>0 then
return foo(n-1)
end
end

该函数支持任意的数字,都不会出现栈溢出。

想要用尾调用的好处,就必须要保证是尾调用。

以下代码都不是尾调用。

function f(x)  g(x)  end        --在调用g后,f还要丢弃g的返回值
return g(x) + 1 --g返回后,还要再加
return x or g(x) --g返回后,还要再或
return (g(x)) --g返回后,还要调整为一个返回值

在Lua中,只有“return func()” 才是尾调用,Lua会在调用前对func和它的参数求值。

比如下面的代码就是尾调用:

return x[i].foo(x[j] + a*b, i+j)