Lua_第 6 章 再论函数

第 6 章 再论函数

      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 sinefunction
a.p(print(1))     --> 0.841470
sin = a.p         --`sin' now refersto 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 标准库提供一个排序函数,接受一个表作为输入参数并且排序表中的元素。这 个函数必须能够对不同类型的值C字符串或者数值)按升序或者降序进行排序。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
 
 
-- writes an `*' atcolumn `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 afunction
-- (assume thatdomain and imageare in the range [-1,1])
function plot (f) eraseTerminal()    
      for i=1,TermSize.w do
      local x = (i/TermSize.w)*2 - 1local y = (f(x)+ 1)/2 * TermSize.h 
      mark(i, y) 
     end
   io.read() --wait before spoilingthe screen
end

要想让这个例子正确的运行,你必须调整你的终端类型和代码中的控制符一致: 

plot(function (x) return math.sin(x*2*math.pi) end)

 将在屏幕上输出一个正弦曲线。将第一类值函数应用在表中是 Lua 实现面向对象和包机制的关键,这部分内容在后 面章节介绍。

6.1 闭包

        当一个函数内部嵌套另一个函数定义时,内部的函数体可以访问外部的函数的局部变量,这种特征我们称作词法定界。虽然这看起来很清楚,事实并非如此,词法定界加 上第一类函数在编程语言里是一个功能强大的概念,很少语言提供这种支持。

        下面看一个简单的例子,假定有一个学生姓名的列表和一个学生名和成绩对应的表; 现在想根据学生的成绩从高到低对学生进行排序,可以这样做:

<pre name="code" class="plain">names = {"Peter", "Paul", "Mary"}
grades = {Mary= 10, Paul = 7,Peter = 8} 
table.sort(names, function (n1, n2)
      return grades[n1] > grades[n2]   -- comparethe grades
end)
 
 

假定创建一个函数实现此功能:

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

       例子中包含在 sortbygrade 函数内部的 sort 中的匿名函数可以访问sortbygrade 的参数 grades,在匿名函数内部 grades 不是全局变量也不是局部变量,我们称作外部的局部变 量(external local variable)或者 upvalue。(upvalue 意思有些误导,然而在 Lua 中他的存 在有历史的根源,还有他比起 external 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

匿名函数使用 upvalue i 保存他的计数,当我们调用匿名函数的时候 i 己经超出了作 用范围,因为创建 i的函数 newCounter 己经返回了。然而 Lua 用闭包的思想正确处理了。这种情况,简单的说闭包是一个函数加上它可以正确访问的 upvalues。如果我们再次调 用 newCounter,将创建一个新的局部变量 i,因此我们得到了一个作用在新的变量 i 上的 新闭包。

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

     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  里的沙箱一样), 当我们运行一段不信任的代码C比如我们运行网络服务器上获取的代码)时安全的环境 是需要的,比如我们可以使用闭包重定义 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 的域C大部分 Lua 标准库使用这种机制来实现的比如 io.read、math.sin)。这种 情况下,必须注意函数和表语法:

1. 表和函数放在一起

<pre name="code" class="plain">Lib = {}
Lib.foo = function (x,y) return x + y end 
Lib.goo = function (x,y) return x - y end 

 
 

2.  使用表构造函数

<pre name="code" class="plain">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()   -- externallocal `f' isvisible here
  ...
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        -- `forward' declarations
 
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

需要注意的是:必须明确什么是尾调用。 一些调用者函数调用其他函数后也没有做其他的事情但不属于尾调用。比如:

function f (x) 
    g(x) 
    return
end

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

return g(x) + 1      -- must do theaddition 
return x or g(x) -- must adjustto 1 result 
return (g(x))  -- must adjustto 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 sameroom 
    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("congratilations!")
end

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值