Welcome

首页 / 软件开发 / 数据结构与算法 / 如何设计一门编程语言(七) 闭包、lambda和interface

如何设计一门编程语言(七) 闭包、lambda和interface2014-08-23人们都很喜欢讨论闭包这个概念。其实这个概念对于写代码来讲一点用都没有,写代码只需要掌握好lambda表达式和class+interface的语义就行了。基本上只有在写编译器和虚拟机的时候才需要管什么是闭包。不过因为系列文章主题的缘故,在这里我就跟大家讲一下闭包是什么东西。在理解闭包之前,我们得先理解一些常见的argument passing和symbol resolving的规则。

首先第一个就是call by value了。这个规则我们大家都很熟悉,因为流行的语言都是这么做的。大家还记得刚开始学编程的时候,书上总是有一道题目,说的是:

void Swap(int a, int b){int t = a;a = b;b = t;}int main(){int a=0;int b=1;Swap(a, b);printf("%d, %d", a, b);}
然后问程序会输出什么。当然我们现在都知道,a和b仍然是0和1,没有受到变化。这就是call by value。如果我们修改一下规则,让参数总是通过引用传递进来,因此Swap会导致main函数最后会输出1和0的话,那这个就是call by reference了。

除此之外,一个不太常见的例子就是call by need了。call by need这个东西在某些著名的实用的函数式语言(譬如Haskell)是一个重要的规则,说的就是如果一个参数没被用上,那传进去的时候就不会执行。听起来好像有点玄,我仍然用C语言来举个例子。

int Add(int a, int b){return a + b;}int Choose(bool first, int a, int b){return first ? a : b;}int main(){int r = Choose(false, Add(1, 2), Add(3, 4));printf("%d", r);}
这个程序Add会被调用多少次呢?大家都知道是两次。但是在Haskell里面这么写的话,就只会被调用一次。为什么呢?因为Choose的第一个参数是false,所以函数的返回值只依赖与b,而不依赖与a。所以在main函数里面它感觉到了这一点,于是只算Add(3, 4),不算Add(1, 2)。不过大家别以为这是因为编译器优化的时候内联了这个函数才这么干的,Haskell的这个机制是在运行时起作用的。所以如果我们写了个快速排序的算法,然后把一个数组排序后只输出第一个数字,那么整个程序是O(n)时间复杂度的。因为快速排序的average case在把第一个元素确定下来的时候,只花了O(n)的时间。再加上整个程序只输出第一个数字,所以后面的他就不算了,于是整个程序也是O(n)。

于是大家知道call by name、call by reference和call by need了。现在来给大家讲一个call by name的神奇的规则。这个规则神奇到,我觉得根本没办法驾驭它来写出一个正确的程序。我来举个例子:

int Set(int a, int b, int c, int d){a += b;a += c;a += d;}int main(){int i = 0;int x[3] = {1, 2, 3};Set(x[i++], 10, 100, 1000);printf("%d, %d, %d, %d", x[0], x[1], x[2], i);}
学过C语言的都知道这个程序其实什么都没做。如果把C语言的call by value改成了call by reference的话,那么x和i的值分别是{1111, 2, 3}和1。但是我们知道,人类的想象力是很丰富的,于是发明了一种叫做call by name的规则。call by name也是call by reference的,但是区别在于你每一次使用一个参数的时候,程序都会把计算这个参数的表达式执行一遍。因此,如果把C语言的call by value换成call by name,那么上面的程序做的事情实际上就是:

x[i++] += 10;
x[i++] += 100;
x[i++] += 1000;

程序执行完之后x和i的值就是{11, 102, 1003}和3了。