lambda函数

我有一篇日志,能让你相信lambda函数。

lambda函数其实就是一种语法糖。

但大多数时候,我觉得它是一种艺术。

1.Anonymous首先简单讲讲概念。如果说lambda函数也通常称为匿名函数,那很多人估计就对lambda函数最基本的用法有了一个大概的轮廓。

最基本的,如果对命名感到了厌倦,或者像我这样对函数名称有纠结症的人,可以使用lambda函数:红色部分定义了一个加1的函数,然后10作为该函数参数,所以如果执行这段scheme代码,结果是11。当然一般人觉得这个例子太蛋疼了,所以下面会更加合情合理一点。

-->    (list 4 9 16)  
-->    4   
-->    4

拿sort函数举例,

-->   (list 3 2)

以上的第一点匿名性质就是大多数人认为lambda函数是一个语法糖的原因。

2.Closure

我已经讲了第一个故事,现在开始讲第二个故事。

历史上最先实现闭包的语言是scheme。至于闭包是什么意思,先看个例子。

这句话定义了一个参数为x的函数g,它的返回值是一个lambda函数,在lambda函数里,对x和y的大小进行比较并返回一个布尔值。

所以如果定义这样就会得到一个长度为5装满lambda函数的list。

最最关键的是,之后的代码里,你可以随便在任何地方使用这些函数而不用再传进去x的值。

--> #t (< 0 1)   
--> #f (< 1 1)

所以闭包指的就是能够让lambda函数读取外围环境的变量,即使调用这个函数的时候已经不在定义它时的作用域里。

Wikipedia上的这句话定义得十分清楚,同时也指出了闭包和一般函数指针的差别,

A closure—unlike a plain function pointer —allows a function to access those non-local variables even when invoked outside of its immediate lexical scope.

在c++ 11里, lambda函数的形式为

[capture](parameters)->return-type{body}

其中capture可以显式指定想要捕获哪些外围环境的变量给这个lambda函数使用。

到这里为止,可能有人觉得闭包这个东西不要也罢,大不了每次多传几个参数,一层层传下去。

但是最后一点我要讲到的会说明为什么闭包重要以及lambda函数在拥有闭包性质的情况下体现出了programming is art。

3.Currying柯理化(其实我挺不喜欢这个山寨般的名字的)充分发挥了闭包的优势。

函数式编程除了因为高阶函数(比如map,reduce,filter等)之外,还因为currying才异常强大。

什么是currying?

Currying is a technique that lets you partially apply a multi-parameter function. When you do that, it remembers those given values, and waits for the remaining parameters.

currying是一种能够让你部分“运行”一个多参数函数的技术。

它能够记住那些已经给了的参数值,执行部分代码,然后等待剩下的参数输入。currying和闭包联系很紧密,一个最最简单的currying例子在前面第二节的那个比较函数里已经见过了。

那个比较函数实际上是两参数的函数,但是因为在第一步定义g的时候给了第一个参数x,之后就会记住x的值并且只需要提供第二个参数。

下面举一个更有说服力的例子。

在写scheme和c之间的binding时,由于c有内存管理而scheme不用担心内存,所以一个重要的问题就是怎么协调内存分配和释放。

scheme如果要调用c的一些函数就需要遵守c的规范,首先分配内存,然后在结束之后释放内存,这对scheme代码来说极为不和谐,而且还会经常忘记。

所以我们可以定义一个trick来做这件事:这个函数给了一块内存大小x,首先分配出一块数据data,初始化为0,然后将传进来的lambda函数f作用于它。

在返回之前首先free了数据,然后把结果r返回。函数的意图很明显,那就是调用函数的人不用关心具体的分配释放过程,而且调用这个函数的话总能保证内存管理的正确性。

问题是f应该具备怎样的性质?

这是一个很generalized的函数,它做的事情就负责调用f和内存分配释放,所以f从道理上来讲应该是任意一个能够对内存数据进行操作的函数。

关键是我们在定义这个with-alloc的时候怎么会知道在未来的某个时间里,f的本体,参数个数,以及参数声明都是什么样子的?

答案是我们不需要知道,因为currying。

比如我们要将sheme中的string类型转换为c的string类型,可以这样做在这里的传入的lambda函数中,str作为外层环境变量将会因为闭包被其之后在with-alloc里使用到。

这里传进来的f是又一个lambda函数,它会对转化之后的c string作另外的操作。

或者我们要将scheme中的vector转化为c中的数组,可以这样做注意在with-vector->c-array中,传入with-array的参数f时,已经有一些变量比如set-element element-size v等已经被部分“运行”并记住了。

所以你可以写无数多个运用currying技巧的并且用到with-alloc内存管理的函数,而不需要每次都更改在with-alloc中对于f的定义。

这样的trick我觉得可以算作是真正意义上的hacking。

4.当然,看到这里肯定会有人说你这些东西某某某语言都有等价实现,实属在此哗众取宠。这是肯定的,因为任何图灵完备的语言都可以相互转换,实现的东西也都一模一样。

你甚至也可以在不支持lambda函数的c语言里通过定义macro来模拟lambda,不过到底怎样才方便,高效,可靠,这个问题就值得仔细思考了。

scheme作为典型的函数式语言,为lambda函数的威力提供了很好的发挥空间。

以后我估计会写一篇关于scheme的日志,讲讲它都能优雅地做些什么事情。

c++ 11现在提供完整的lambda函数支持,python提供简单的函数支持,还有很多很多语言都支持lambda。

所以即使你个人觉得函数式语言蛋疼,但也可以考虑用用这些大众语言的lambda。

我的文章写完了,至于选不选择lambda函数,一切都交给你。

上面说到了C++中lambda的基本形式为

1
[capture](parameters) mutable ->return-type{statement}

与普通函数最大的区别是,除了可以使用参数以外,Lambda函数还可以通过捕获列表访问一些上下文中的数据。

具体地,捕捉列表描述了上下文中哪些数据可以被Lambda使用,以及使用方式(以值传递的方式或引用传递的方式)。

语法上,在“[]”包括起来的是捕捉列表,捕捉列表由多个捕捉项组成,并以逗号分隔。捕捉列表有以下几种形式:

1.[var]表示值传递方式捕捉变量var;

2.[=]表示值传递方式捕捉所有父作用域的变量(包括this);

3.[&var]表示引用传递捕捉变量var;

4.[&]表示引用传递方式捕捉所有父作用域的变量(包括this);

5.[this]表示值传递方式捕捉当前的this指针。

上面提到了一个父作用域,也就是包含Lambda函数的语句块,说通俗点就是包含Lambda的“{}”代码块。上面的捕捉列表还可以进行组合,例如:

[=,&a,&b]表示以引用传递的方式捕捉变量a和b,以值传递方式捕捉其它所有变量;

[&,a,this]表示以值传递的方式捕捉变量a和this,引用传递方式捕捉其它所有变量。

不过值得注意的是,捕捉列表不允许变量重复传递。下面一些例子就是典型的重复,会导致编译时期的错误。例如:

[=,a]这里已经以值传递方式捕捉了所有变量,但是重复捕捉a了,会报错的;

[&,&this]这里&已经以引用传递方式捕捉了所有变量,再捕捉this也是一种重复。

script>