在Lua中如何动态生成两个函数的复合函数

    Lua是一种动态类型语言,变量名可以在运行时绑定到任意类型的值上。Lua从函数式编程语言中借鉴了许多思想,这体现在Lua的基本类型中除了有number,string等其它语言中常见的基本类型外,还有函数(function)。这意味着函数: <1> 可以用变量命名; <2>可以提供给函数作为参数; <3> 可以作为函数的返回值; <4> 可以包含在数据结构中。下面我们利用函数在Lua中作为"一等公民"的身份,来实现一个简单的泛函操作————函数复合。

    假设有这样两个函数f: D1 × D2 × D3 × ... × Dn -> R(这表明函数f的定义域是集合D1到Dn的笛卡尔积的子集,值域是R的子集),g: R -> S,那么函数f与g就是可复合的,并且它们的复合函数h满足如下要求,它的定义域与f一样,值域与g一样,并且对于每个定义域中的元素e来讲,h(e)的值与g(f(e))的值相等。我们的目标是定义一个完成复合动作的函数,它的输入是任意两个满足可复合条件的函数,输出则是它们的复合函数。下面是一个具体的实现:

function compose1(f,g)
  local newfn =
         function (...)
           local old_func = f

           --@因为old_func的参数个数不确定,
           -- 所以动态构造调用此函数的程序文本
           --@利用了Lua的动态编译功能
           -- 及debug模块提供的反射机制
           local s = "local name,old_fn,old_fn_arg/n"
           -- .. 是Lua的字符串连接操作符
           s = s.."name,old_fn_arg = debug.getlocal(2,1)/n"
           s = s.."name,old_fn = debug.getlocal(2,2)/n"
           s = s.."return old_fn("
           for i,v in ipairs(arg) do
             s = s..string.format("old_fn_arg[%d],",i)
           end
           s = s.."nil)"

           local dynamic_func = assert(loadstring(s))
               
           return g(dynamic_func())
         end

  return newfn
end

    在compose1函数体中定义(实际是创建,就象创建其它类型对象一样)了一个新的且参数个数不确定的匿名函数,并把它与局部变量名fn绑定起来,这就意味着以后fn这个名字代表的是这个刚定义的函数,除非你又将fn与其它对象绑定。我们看到这种绑定是通过赋值操作符来实现的,与变量名绑定到其它基本类型值的操作方法完全相同。如果定义了一个参数个数不确定的函数,那么系统会在这个函数被调用的时候自动将所有实际参数封装到一个列表结构里,并自动把列表绑定到函数体中的局部变量名arg(系统隐式定义)上。新定义函数中的局部变量名s代表一个字符串,它存放着根据实际调用时传递的参数个数而动态展开的f函数调用程序文本(因为f的参数事先不确定,所以我们没法在程序中写成f(a,b)这样的调用形式)。loadstring(s)的返回值(被绑定到dynamic_func上)是一个函数,这个函数的body是经由动态编译s字符串而得来的,但是这个函数的环境(enviroment,可以认为是函数所能访问的所有全局变量的集合)被设定为系统的全局环境,所以在执行dynamic_func函数时,它不知道arg,old_func这样的变量名代表什么意思(它们是newfn函数的局部变量,全局环境中没有它们),所以我们插进了诸如debug.getlocal(..)这样的函数调用以取得newfn函数中的局部变量的值。比如name,old_fn_arg = debug.getlocal(2,1)执行起来就是获取调用堆栈中第二层函数的第一个局部变量的名字与值,也就是说name取得了newfn函数中的第一个局部变量arg的名字"arg",而old_fn_arg则得到它的值,即被封装的newfn的不定参数列表。同样道理name,old_fn = debug.getlocal(2,2)取得了newfn中第二个局部变量old_func的名字("old_func")与值(就是传给compose1形参f的值,因为在newfn函数体里有local old_func = f的赋值动作)。这样在动态编译生成的函数dynamic_func中就可以访问到外部传进的参数,从而达到动态调用的目的。通过newfn函数体中最后一条返回语句使得每次调用newfn的效果与执行g(dynamic_func())这么一个复合一样,接着newfn函数本身就被当作返回值而传出。

    可以通过如下代码来测试compose1函数是否能正确工作:

function double(x)
    return x * 2
end

-- add all parameters
-- and return the sum
function add(...)
    local sum = 0   
    for i,v in ipairs(arg) do
       sum = sum + v
    end
    return  sum
end

f1 = compose1(double,print)
f2 = compose1(add,print)

f1(18) 
-- 打印出36

f2(1,2,3,4,5,6,7,8,9,10)
--打印出55

    上面的compose1能够很好地工作,但是生成的复合函数效率肯定不会太高。因为每次调用结果函数时都要执行动态编译过程(由loadstring(..)引起),这是相当耗资源的动作。那么有办法避免动态编译吗?事实上,之所以要在生成的复合函数中插入loadstring(..)是因为f函数的参数个数不确定,我们无法在程序当中以一种固定的格式写下调用代码。比如当你写f(arg[1],arg[2],...,arg[n])时,你就丧失了处理参数个数多于n的函数的能力。幸运的是,Lua不仅能够自动将可变个数参数封装成列表,也提供了函数将列表还原为原本的以逗号分隔的形式,这个函数就是unpack。当你写下unpack(arg)时,就相当于写下arg[1],arg[2],...,arg[n](n为列表的实际元素个数),只不过你只能针对一个固定的常量n写,而unpack函数却能根据arg中实际元素个数来"写"。现在我们可以利用unpack来改进上面那个compose1:

function compose2(f,g)
   local newfn =
          function (...)
            return g(f(unpack(arg)))
          end

   return newfn
end

    可以看到,新的实现版本非常简单,这完全拜unpack所赐,它的强大表达能力为操作参数个数未知的函数提供了极大的方便,也使编写更复杂的泛函操作成为可能。

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页