烦恼一般都是想太多了。

0%

PIL.9Lua中的闭包

Lua中的函数是 第一类的值加上合适的词法域。那么,函数是第一类值意味着什么呢?这是说,在Lua中,一个函数,就是一个和 数字或字符串一样具有某些权限的值。一个程序可以把函数存储在变量中(全局或者局部都可以)和表中,把函数作为参数传递给其他函数,或者以函数作为值返回。

函数具有词法域又是什么意思呢?就是说函数可以访问他们包裹函数的变量。

这两个特性给了Lua巨大的弹性;具体点说,一个程序在运行一段不受信任的代码时(比如从网络上收到的代码)可以重新定义一个函数来增加功能或者擦除一个函数来创建一个安全的环境。更重要的是,这些特性运行我们从函数语言的世界应用很多强大的编程技术到Lua。即使你对函数式编程没有什么兴趣,但是看一下这些技术是非常有价值的,这会让你的程序更小更简单。

函数作为第一类值

下面的例子说明了函数作为第一类值的情况:

a = {p = print} -- 'a.p' refers to the 'print' function
a.p("hello world") -- hello world
print = math.sin -- 'print' now refers to the sine function
a.p(print(1)) -- 0.8414709848079
math.sin = a.p -- 'sin' no refers to the print function
math.sin(10, 20) -- 20

如果函数是值,这些表达式会创建函数么?当然。实际上,在Lua经常编写函数的方式:

function foo (x) return 2*x end

其实只是一个被我们称作 语法糖的东西;其只是下面代码的一个比较漂亮的方式:

foo = function (x) return 2*x end

在表达式右边的赋值部分function (x) return 2*x end是一个函数构造器,同样的方式{ }是一个表构造器。这就是说,一个函数定义,实际上就是一个建立一个类型为function的值并把它赋给一个变量的声明。

注意,在Lua中,所有的函数都是匿名的。和其他值一样,他们没有名字。当我们在谈论一个函数名字的时候,比如print,其实我们是在谈论存储函数的变量。尽管我们经常把函数赋值给全局变量,似乎给了他们一个名字,仍然有几种情况函数会保持匿名。我们来看看例子。

基本库中,表这个库提供了一个函数table.sort,其会接受一个表,然后排序表的元素。这样的函数必须能接受不受限制的排序方式:升序或降序,数字的或字母的,以键排序表等等。没有尝试提供所有的选项类型,sort提供了一个单一定选项order function(排序函数):一个获取两个元素,然后返回第一个是否要在排序好的表中比第二个元素先出现。看看下面的例子:

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)

这个声明内可以匿名函数是非常方便的。

一个函数以其他函数作为参数,如sort,我们称它为 高层函数。高层函数是一个强大的编程方法,而以匿名函数来建立他们的函数参数又非常具有弹性。但要记住,高层函数没有特殊的权限;Lua会把所有函数当作第一类值。

为了更多的展示一下高层函数,我们来写一个常用的高层函数,导数函数。在一个正式的定义中,函数 f 的导数是当 d 变得无穷小时的函数 f’(x) = (f(x + d) - f(x)) / d。根据这个定义,我们可以如下计算出一个近似导数:

function derivative (f, delta)
delta = deleta or 1e-4
return function (x)
return (f(x + deleta) - f(x))/deleta
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

非全局函数

一个很明显的结论就是,我们不但可以把函数存储在全局变量,而且也可以存在在表字段或者局部变量内。

我们已经看到了很多个把函数放在表字段内的例子:大多数Lua库使用这种方法(如 io.read, math.sin)。为了建立这样的函数:

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

Lua,也提供了特别的方式来定义这样的函数:

Lib = {}
function Lib.foo (x,y) return x + y end
function Lib.goo (x,y) return x - y end

lua中的面向对象一节中,我们可以看到,把函数放在表字段中是实现面向对象的重要部分。

当我们把函数存储在一个局部变量中时,我们获得了一个 局部函数 就是说这个函数被限制在给定的范围内。这样的定义对于 来说是非常有用的:因为Lua把每个 chunk 当作函数处理,一个 chunk可以定义 局部函数,其只在当前的 chunk内可见。词法域 保证了这个 chunk内的其他函数可以访问这个 局部函数。

Lua这样以一个语法糖的方式使用局部函数:

local function f (params)
body
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并没有定义完成。因此,这个表示式会尝试调用一个全局的 fact,而不是本地的这个。我们可以通过先定义变量,然后再定义函数体来避免这个错误:

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

现在 函数内的fact就引用本地的变量。在定义函数的时候其值不重要,在函数执行时,fact会有正确的值。

当Lua展开其对局部函数的语法糖时,其不使用这种写法。这样的定义 :

local function foo (params) body end

会展开成:

local foo; foo = function (params) body end

因此,我们可以这样来使用递归函数而不用有其他担心。

当然,这在我们使用非直接的递归函数时不会起作用。这样的情况下,我们必须使用一种显式的声明:


local f -- "forward" declaration

local function g ()
some code
f()
some code
end

function f ()
some code
g ()
some code
end

注意,在最后一个第一中不要写 local。否则,Lua会创建一个新的 本地变量 f ,并使 原来 的 f 变成未定义的。

词法域

在我们写一个被其他函数包围的函数时,其可以完全的访问包围函数的变量;我们把这个特性叫做 词法定界。 这个可见性规则听起来可能很明显,但不是的。词法定界加上嵌套的第一类函数,给了Lua很大的力量,但很多语言并不支持这样的结合。

我们以一个简单的例子开始。我们有一个学习名字的表,已经一张名字和学位等级映射的表;我们想通过学生的学位来排序学生名字那张表,高学位的在前。我们可以向下面这样做:

names = {"Peter", "Paul", "Mary"}
grades = {Mary = 10, Paul = 7, Peter = 8}
table.sort(names, 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

后面这个例子有趣的一点就是,sort中的匿名函数访问了 grades ,而这是 包围函数 sortbygrade 的参数。 在匿名函数中, grades 不是一个全局变量,也不是一个局部变量,我们把它叫做 非局部变量。(因为历史原因,非局部变量,也被叫做 上值(upvalues)

我们这点会非常有趣?因为函数,是第一类值,可以 逃脱 其变量的原始范围。看一下下面的代码:

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

c1 = newCounter()

print(c1()) -- 1
print(c1()) -- 2

在这些代码中,匿名函数引用了一个非局部变量count ,来保持其计数器。然而,在我们调用这个匿名函数的时候,变量 count 看起来已经在其范围之外了, 因为建立这个变量的函数 newCounter 已经返回了。然而,Lua会正确的处理这样的情况,使用了closure(闭包)的概念。 简单地说, 闭包 就是一个函数 加上其需要访问的所有 非局部变量。 如果我们再次调用 newCounter,其会建立一个新的局部变量 count 并加上一个新的闭包,不在新的变量上作用:


c2 = newCounter()
print(c2()) -- 1

print(c1()) -- 3
print(c2()) -- 2

因此, c1, c2是不同的闭包。他们都是一同一个函数来建立,但是每个在不同的 局部变量 count 上动作。

技术上讲,Lua中的值是闭包,而不是这个函数。函数只是闭包的一种原型。但是呢,在不混淆的情况下, 我们会继续使用 函数 来引用一个闭包。

闭包在很多上下文中非常有价值。如我们所见,其作为高层函数的参数非常有用,比如sort。 闭包对于建立其他函数的函数也非常有价值,如 newCounter或 导数例子;这和方法允许Lua程序把高端编程技术和函数世界相结合。闭包对 回调(callback)函数也很有用。一个典型的例子就是在我们在GUI工具中建立一个按钮的时候。每个按钮都在用户按这个按钮时调用一个 回调函数 ;但我们需要每个按钮干不同的事情。

具体来说,一个数字计算器需要10个类似的按钮,每个数字一个。我们可以通过一个函数来创建他们:

function diginButton (digit)
return Button { lable = tostring(digit),
action = function ()
add_to_display(digit)
end
}
end

在这个例子中,我们假装 Button是一个创建新按钮的工具函数;label 是按钮的标签; action 是按下按钮时的回调函数。 回调函数可能会在 digitButton完成任务后很久才会被调用,但其仍然可以访问 digit 变量。

闭包在不同的上下文中也很有价值。因为函数存储于普通的变量中,我们可以在Lua重新定义函数,即使是预定义的函数。这也是为什么Lua如此扩展性好的原因之一。假如我们想要重新定义 sin来操作角度而不是弧度。这个新函数把其参数进行转换然后调用原来的 sin函数来做真正的工作。代码类似下面:

local 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

代码使用 do .. end来限制本地变量 oldSin 的词法范围;其只在当前 chunk内可见。其只能通过 新函数来访问。

可以用同样的方法来建立安全的环境,也叫做沙盒。安全环境在运行不受信任的歹时非常重要,这样的代码通过服务从网络获得。具体来说,为了限制一个程序可以访问的文件,我们可以用闭包来重新定义 io.open函数:

do
local oldOpen = io.open
local access_OK = function (filename, mode)
check_access
end
io.open = function (filename, mode)
if access_OK(filename, mode)
return oldOpen(filename, mode)
else
return nil, "access dinied"
end
end
end

我们让这个例子变得很好的地方是在这重新定义后,没有其他方式来调用不受限制的 io.open函数版本,其只能通过新函数来访问。其将不受限制的版本在闭包中以一个变量保存,外部将不能访问。通过这样的技术,我们可以在Lua自身建立沙盒,有常用的好处:简单 和弹性。Lua提供了 meta-mechanism,而不是 one-size-fits-all的方式,我们可以根据我们的需求来定义我们的环境。

函数编程的好处

为了给予更多函数编程的例子,我们来开发一个几合形状的简单系统。目的是开发一个表示几何形状的系统,一个形状是一系列点的集合。我们需要能表示所有类型的形状,并以几种方式来结合和修改形状。

为了实现这个系统,我们要寻找好的数据结构来表达形状;我们可以尝试面向对象的方式并定义一些形状的层级。或者我们可以在更高的抽象层面工作,并通过形状的特点函数来表示我们的设置。

每个几何区域都是一系列点的集合,我们可以通过特征函数来代表一个区域;这就是说,我们可以通过一个函数来代表一个区域,这个函数得到一个点后,会返回这个点是否属于区域内。

下面的函数代表了一个圆,中心(1.0, 3.0),半径 4.5:

function disk1 (x,y)
return (x - 1.0)^2 + (y - 3.0)^2 <= 4.5^2
end

集合高层函数和词法定界,很容易定义滚个 圆盘工厂,其以给定的半径和中心来建立圆盘:

function disk (cx, cy, r)
return function (x, y)
return (x - cx)^2 + (y - cy)^2 <= r^2
end
end

调用 disk(1.0, 3.0, 4.5)和 disk1一样。