lua闭包

一、闭包的由来
闭包这个概念对于没有接触过函数式编程的人来说还是比较陌生的,它基于把函数看作头等公民(first-class),至于怎么理解这个first-class,我想大致就是说把函数看作是像int、float这样的基本类型,而不是把它看作一个特殊的、定制的特殊实体。把函数当作基本类型之后,就可以把它们作为函数的返回值、参数、存放在变量中等这些之前只是对于基本类型适应的操作。当然这个只是一个粗略的概念描述,在真正实现这种通用的时候还是有些问题需要考虑。
下面是一个对新手来说并不太简单的例子:
tsecer@harry: cat lua-closure.lua 
function genfun(x,y) 
 
local lf = function (lx, ly)
return ly * 10
end
 
local z;
z = lf(x, x *x + x * x)
 
if (x == 1)
then
return function (xx, yy) return  z + x, z - x, z * x end 
else
return function (xxx, yyy) return z + xxx end
end
end
 
local inst11 = genfun(1, 1)
local inst22 = genfun(2, 2)
local i1, i2, i3;
i1, g1, i2, i3 = inst11(3, 3)
print(i2, g1, i1)
print(inst22(4, 4))
 
tsecer@harry: 
这里看到的一个现象是genfun函数返回了匿名函数,并且这些函数使用了自己外层函数的局部变量,这些局部变量可能在函数返回之后已经不存在了。genfun函数根据参数的不同动态的返回不同的函数定义,它们返回值数量相同。
二、upvalue的意义
那么如何解决函数使用局部变量的问题?这里解决的方法是将每个函数依赖的外部变量“局部化”,和“自干五”的思路类似,返回的函数可以自带自己需要的所有外部变量(的存储空间或者位置信息),当返回这个函数定义的时候将这些依赖变量一并返回。这里返回的结构就是一个包含了自己运行环境的闭包(Closure),而这些用来保证闭包可以运行的变量则成为upvalue。
关于upvalue这个名字本身的命名,作者自己也有一些吐槽
We call it an external local variable, or an upvalue. (The term "upvalue" is a little misleading, because grades is a variable, not a value. However, this term has historical roots in Lua and it is shorter than "external local variable".)
the concept of upvalues, which allowed nested functions to access the values of external variables, but not the variables themselves
三、upvalue的处理
1、upvalue的编译时处理
一个函数是否使用了外部变量,使用了哪些外部变量,这些其实更多的是一个词法变量的查找过程,所以这个过程在编译时确定。在语法解析时,如果遇到一个TK_NAME类型的词法单位,此时需要查找该变量是否有定义,这个过程就是通过singlevaraux函数完成,它本身包含了递归逐层向上查找的过程。
static void singlevaraux (FuncState *fs, TString *n, expdesc *var, int base) {
  if (fs == NULL)  /* no more levels? */
    init_exp(var, VVOID, 0);  /* default is global */
  else {
    int v = searchvar(fs, n);  /* look up locals at current level */
    if (v >= 0) {  /* found? */
      init_exp(var, VLOCAL, v);  /* variable is local */
      if (!base)
        markupval(fs, v);  /* local will be used as an upval */
    }
    else {  /* not found as local at current level; try upvalues */
      int idx = searchupvalue(fs, n);  /* try existing upvalues */
      if (idx < 0) {  /* not found? */
        singlevaraux(fs->prev, n, var, 0);  /* try upper levels */
        if (var->k == VVOID)  /* not found? */
          return;  /* it is a global */
        /* else was LOCAL or UPVAL */
        idx  = newupvalue(fs, n, var);  /* will be a new upvalue */
      }
      init_exp(var, VUPVAL, idx);  /* new or old upvalue */
    }
  }
}
 
在递归的过程中,注意有一个细节,就是
        if (var->k == VVOID)  /* not found? */
          return;  /* it is a global */
        /* else was LOCAL or UPVAL */
        idx  = newupvalue(fs, n, var);  /* will be a new upvalue */
这意味着如果定义处和该函数之间有间隔,这里会架起中间所有空隙的桥梁,还是举个例子:
tsecer@harry: cat -n lua-bridge.lua
     1 function f(x)
     2 return function ()
     3 return function () return x * x end
     4 end
     5 end
这个例子中,最内存函数引用了最外层的参数x,中间层并没有引用,前面的newupvalue将会在中间层创建一个对upvalue的引用,从而保证薪火相传,生生不息。这一点可以从生成的机器指令中看到
tsecer@harry:   /home/tsecer/Download/lua-5.3.4/src/luac -l -l  lua-bridge.i
 
main <./lua-bridge.lua:0,0> (3 instructions at 0x8efdf40)
0+ params, 2 slots, 1 upvalue, 0 locals, 1 constant, 1 function
1 [5] CLOSURE   0 0 ; 0x8efe018
2 [1] SETTABUP  0 -1 0 ; _ENV "f"
3 [5] RETURN    0 1
constants (1) for 0x8efdf40:
1 "f"
locals (0) for 0x8efdf40:
upvalues (1) for 0x8efdf40:
0 _ENV 1 0
 
function <./lua-bridge.lua:1,5> (3 instructions at 0x8efe018)
1 param, 2 slots, 0 upvalues, 1 local, 0 constants, 1 function
1 [4] CLOSURE   1 0 ; 0x8efe090
2 [4] RETURN    1 2
3 [5] RETURN    0 1
constants (0) for 0x8efe018:
locals (1) for 0x8efe018:
0 x 1 4
upvalues (0) for 0x8efe018:
 
function <./lua-bridge.lua:2,4> (3 instructions at 0x8efe090)
0 params, 2 slots, 1 upvalue, 0 locals, 0 constants, 1 function
1 [3] CLOSURE   0 0 ; 0x8efe118
2 [3] RETURN    0 2
3 [4] RETURN    0 1
constants (0) for 0x8efe090:
locals (0) for 0x8efe090:
upvalues (1) for 0x8efe090:
0 x 1 0
 
function <./lua-bridge.lua:3,3> (5 instructions at 0x8efe118)
0 params, 2 slots, 1 upvalue, 0 locals, 0 constants, 0 functions
1 [3] GETUPVAL  0 0 ; x
2 [3] GETUPVAL  1 0 ; x
3 [3] MUL       0 0 1
4 [3] RETURN    0 2
5 [3] RETURN    0 1
constants (0) for 0x8efe118:
locals (0) for 0x8efe118:
upvalues (1) for 0x8efe118:
0 x 0 0
其中中间函数对应的指令 function <./lua-bridge.lua:2,4> (3 instructions at 0x8efe090) 中包含了把x作为upvalue的指令。
2、函数定义对于虚拟机指令生成的影响
当遇到函数定义是,会在函数定义位置生成一个OP_CLOSURE指令,其第一个参数表示指令返回的Closure结构存放的寄存器编号,第二个参数表示该函数内定义的函数的编号。
static void codeclosure (LexState *ls, expdesc *v) {
  FuncState *fs = ls->fs->prev;
  init_exp(v, VRELOCABLE, luaK_codeABx(fs, OP_CLOSURE, 0, fs->np - 1));
  luaK_exp2nextreg(fs, v);  /* fix it at the last register */
}
3、OP_CLOSURE指令的虚拟机执行
      vmcase(OP_CLOSURE) {
        Proto *p = cl->p->p[GETARG_Bx(i)];
        LClosure *ncl = getcached(p, cl->upvals, base);  /* cached closure */
        if (ncl == NULL)  /* no match? */
          pushclosure(L, p, cl->upvals, base, ra);  /* create a new one */
        else
          setclLvalue(L, ra, ncl);  /* push cashed closure */
        checkGC(L, ra + 1);
        vmbreak;
      }
……
static void pushclosure (lua_State *L, Proto *p, UpVal **encup, StkId base,
                         StkId ra) {
  int nup = p->sizeupvalues;
  Upvaldesc *uv = p->upvalues;
  int i;
  LClosure *ncl = luaF_newLclosure(L, nup);
  ncl->p = p;
  setclLvalue(L, ra, ncl);  /* anchor new closure in stack */
  for (i = 0; i < nup; i++) {  /* fill in its upvalues */
    if (uv[i].instack)  /* upvalue refers to local variable? */
      ncl->upvals[i] = luaF_findupval(L, base + uv[i].idx);
    else  /* get upvalue from enclosing function */
      ncl->upvals[i] = encup[uv[i].idx];
    ncl->upvals[i]->refcount++;
    /* new closure is white, so we do not need a barrier here */
  }
  if (!isblack(p))  /* cache will not break GC invariant? */
    p->cache = ncl;  /* save it on cache for reuse */
}
创建Closure的时候,判断如果这个upvalue是自己定义的(instack)则创建,否则建立到自己上层栈帧中的引用(由于singlevaraux保证了这个链表一定是存在的,所以只要和自己的紧邻上层栈帧建立链接即可,这相当于把问题divide and conquer到各个栈帧中)。
4、Closure的关闭
在一个Closure创建的时候,它保存的只是到真实变量的引用,直到该Closure所在的作用域结束(例如函数调用的return、for循环中执行完一次循环),Closure引用的upvalue的真实值才最终确定,用Lua作者中的例子来说明这个问题http://www.cs.tufts.edu/~nr/cs257/archive/roberto-ierusalimschy/closures-draft.pdf:
tsecer@harry: cat lua-close.lua
local a = {} -- an empty array
local x = 10
for i = 1, 2 do
local j = i
a[i] = function () return x + j end
end
x = 20
 
print(a[1]())
print(a[2]())
tsecer@harry:   /home/tsecer/Download/lua-5.3.4/src/lua lua-close.lua
21
22
tsecer@harry: 
可以看到x最终获得的值并不是Closure创建时的10,而是关闭时的20。
四、C Closure
CClosure也可以使用自己的upvalues,它其实可以和Lua Closure的理解一样,起始就是一个函数附属的运行时变量,它们依附在Closure中,和Closure绑定,从而让Closure有一些运行时状态。同样是用Lua官方的例子说明
在创建的时候,首先通过push将upvalue的初始值压入堆栈,然后调用lua_pushcclosure来创建CClosure,lua_pushcclosure函数在分配完CClosure之后会从堆栈中拷贝初始值到自己的upvalue空间列表中。
    /* forward declaration */
    static int counter (lua_State *L);
    
    int newCounter (lua_State *L) {
      lua_pushnumber(L, 0);
      lua_pushcclosure(L, &counter, 1);
      return 1;
    }
然后在具体的C函数中就可以通过lua_tonumber(L, lua_upvalueindex(1))来访问自带的upvalue(由于lua_upvalueindex的特殊处理,lua_tonumber会知道从CClosure的upvalue中取值、其它的存取方式还包括有从相对栈底偏移,相对栈顶偏移、注册表偏移)
   static int counter (lua_State *L) {
      double val = lua_tonumber(L, lua_upvalueindex(1));
      lua_pushnumber(L, ++val);  /* new value */
      lua_pushvalue(L, -1);  /* duplicate it */
      lua_replace(L, lua_upvalueindex(1));  /* update upvalue */
      return 1;  /* return new value */
    }
附录:
开始代码的反编译内容,放在这里作为备忘:
tsecer@harry: cat -n lua-code.lua
     1 function genfun(x,y) 
     2
     3 local lf = function (lx, ly)
     4 return ly * 10
     5 end
     6
     7 local z;
     8 z = lf(x, x *x + x * x)
     9
    10 if (x == 1)
    11 then
    12 return function (xx, yy) return  z + x, z - x, z * x end 
    13 else
    14 return function (xxx, yyy) return z + xxx end
    15 end
    16 end
    17
    18 local inst11 = genfun(1, 1)
    19 local inst22 = genfun(2, 2)
    20 local i1, i2, i3;
    21 i1, g1, i2, i3 = inst11(3, 3)
    22 print(i2, g1, i1)
    23 print(inst22(4, 4))
tsecer@harry:   /home/tsecer/Download/lua-5.3.4/src/lua lua-code.lua
20 19 21
84
tsecer@harry:   /home/tsecer/Download/lua-5.3.4/src/luac -o lua-code.i lua-code.lua
tsecer@harry:   /home/tsecer/Download/lua-5.3.4/src/luac -l -l  lua-code.i
 
main <lua-code.lua:0,0> (31 instructions at 0x9a16f30)
0+ params, 9 slots, 1 upvalue, 5 locals, 7 constants, 1 function
1 [16] CLOSURE   0 0 ; 0x9a17100
2 [1] SETTABUP  0 -1 0 ; _ENV "genfun"
3 [18] GETTABUP  0 0 -1 ; _ENV "genfun"
4 [18] LOADK     1 -2 ; 1
5 [18] LOADK     2 -2 ; 1
6 [18] CALL      0 3 2
7 [19] GETTABUP  1 0 -1 ; _ENV "genfun"
8 [19] LOADK     2 -3 ; 2
9 [19] LOADK     3 -3 ; 2
10 [19] CALL      1 3 2
11 [20] LOADNIL   2 2
12 [21] MOVE      5 0
13 [21] LOADK     6 -5 ; 3
14 [21] LOADK     7 -5 ; 3
15 [21] CALL      5 3 5
16 [21] MOVE      4 8
17 [21] MOVE      3 7
18 [21] SETTABUP  0 -4 6 ; _ENV "g1"
19 [21] MOVE      2 5
20 [22] GETTABUP  5 0 -6 ; _ENV "print"
21 [22] MOVE      6 3
22 [22] GETTABUP  7 0 -4 ; _ENV "g1"
23 [22] MOVE      8 2
24 [22] CALL      5 4 1
25 [23] GETTABUP  5 0 -6 ; _ENV "print"
26 [23] MOVE      6 1
27 [23] LOADK     7 -7 ; 4
28 [23] LOADK     8 -7 ; 4
29 [23] CALL      6 3 0
30 [23] CALL      5 0 1
31 [23] RETURN    0 1
constants (7) for 0x9a16f30:
1 "genfun"
2 1
3 2
4 "g1"
5 3
6 "print"
7 4
locals (5) for 0x9a16f30:
0 inst11 7 32
1 inst22 11 32
2 i1 12 32
3 i2 12 32
4 i3 12 32
upvalues (1) for 0x9a16f30:
0 _ENV 1 0
 
function <lua-code.lua:1,16> (17 instructions at 0x9a17100)
2 params, 8 slots, 0 upvalues, 4 locals, 1 constant, 3 functions
1 [5] CLOSURE   2 0 ; 0x9a171c0
2 [7] LOADNIL   3 0
3 [8] MOVE      4 2
4 [8] MOVE      5 0
5 [8] MUL       6 0 0
6 [8] MUL       7 0 0
7 [8] ADD       6 6 7
8 [8] CALL      4 3 2
9 [8] MOVE      3 4
10 [10] EQ        0 0 -1 ; - 1
11 [10] JMP       0 3 ; to 15
12 [12] CLOSURE   4 1 ; 0x9a17298
13 [12] RETURN    4 2
14 [12] JMP       0 2 ; to 17
15 [14] CLOSURE   4 2 ; 0x9a173e8
16 [14] RETURN    4 2
17 [16] RETURN    0 1
constants (1) for 0x9a17100:
1 1
locals (4) for 0x9a17100:
0 x 1 18
1 y 1 18
2 lf 2 18
3 z 3 18
upvalues (0) for 0x9a17100:
 
function <lua-code.lua:3,5> (3 instructions at 0x9a171c0)
2 params, 3 slots, 0 upvalues, 2 locals, 1 constant, 0 functions
1 [4] MUL       2 1 -1 ; - 10
2 [4] RETURN    2 2
3 [5] RETURN    0 1
constants (1) for 0x9a171c0:
1 10
locals (2) for 0x9a171c0:
0 lx 1 4
1 ly 1 4
upvalues (0) for 0x9a171c0:
 
function <lua-code.lua:12,12> (11 instructions at 0x9a17298)
2 params, 6 slots, 2 upvalues, 2 locals, 0 constants, 0 functions
1 [12] GETUPVAL  2 0 ; z
2 [12] GETUPVAL  3 1 ; x
3 [12] ADD       2 2 3
4 [12] GETUPVAL  3 0 ; z
5 [12] GETUPVAL  4 1 ; x
6 [12] SUB       3 3 4
7 [12] GETUPVAL  4 0 ; z
8 [12] GETUPVAL  5 1 ; x
9 [12] MUL       4 4 5
10 [12] RETURN    2 4
11 [12] RETURN    0 1
constants (0) for 0x9a17298:
locals (2) for 0x9a17298:
0 xx 1 12
1 yy 1 12
upvalues (2) for 0x9a17298:
0 z 1 3
1 x 1 0
 
function <lua-code.lua:14,14> (4 instructions at 0x9a173e8)
2 params, 3 slots, 1 upvalue, 2 locals, 0 constants, 0 functions
1 [14] GETUPVAL  2 0 ; z
2 [14] ADD       2 2 0
3 [14] RETURN    2 2
4 [14] RETURN    0 1
constants (0) for 0x9a173e8:
locals (2) for 0x9a173e8:
0 xxx 1 5
1 yyy 1 5
upvalues (1) for 0x9a173e8:
0 z 1 3
tsecer@harry: 

 

posted on 2019-03-07 10:32  tsecer  阅读(355)  评论(0编辑  收藏  举报

导航