如何设计一门语言(七)——闭包、lambda和interface

人们都很喜欢讨论闭包这个概念。其实这个概念对于写代码来说一点用都没有,写代码只须要掌握好lambda表达式和class+interface的语义就好了。基本上只有在写编译器和虚拟机的时候才须要管什么是闭包。不过由于系列文章主题的缘故,在这里我就跟你们讲一下闭包是什么东西。在理解闭包以前,咱们得先理解一些常见的argument passing和symbol resolving的规则。javascript

首先第一个就是call by value了。这个规则咱们你们都很熟悉,由于流行的语言都是这么作的。你们还记得刚开始学编程的时候,书上老是有一道题目,说的是:java

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了。python

除此以外,一个不太常见的例子就是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了。闭包

很神奇对吧,稍微不注意就会中招,是个大坑,基本无法用对吧。那大家还成天用C语言的宏来代替函数干什么呢。我依稀记得Ada有网友指出这是Algol 60)仍是什么语言就是用这个规则的,印象比较模糊。异步

讲完了argument passing的事情,在理解lambda表达式以前,咱们还须要知道两个流行的symbol resolving的规则。所谓的symbol resolving讲的就是解决程序在看到一个名字的时候,如何知道这个名字到底指向的是谁的问题。因而我又能够举一个简单粗暴的例子了:函数

Action<int> SetX()
{
    int x = 0;
    return (int n)=>
    {
        x = n;
    };
}

void Main()
{
    int x = 10;
    var setX = SetX();
    setX(20);
    Console.WriteLine(x);
}

弱智都知道这个程序其实什么都没作,就输出10。这是由于C#用的symbol resolving地方法是lexical scoping。对于SetX里面那个lambda表达式来说,那个x是SetX的x而不是Main的x,由于lexical scoping的含义就是,在定义的地方向上查找名字。那为何不能在运行的时候向上查找名字从而让SetX里面的lambda表达式实际上访问的是Main函数里面的x呢?实际上是有人这么干的。这种作法叫dynamic scoping。咱们知道,著名的javascript语言的eval函数,字符串参数里面的全部名字就是在运行的时候查找的。

=======================我是背景知识的分割线=======================

想必你们都以为,若是一个语言的lambda表达式在定义和执行的时候采用的是lexical scoping和call by value那该有多好呀。流行的语言都是这么作的。就算规定到这么细,那仍是有一个分歧。到底一个lambda表达式抓下来的外面的符号是只读的仍是可读写的呢?python告诉咱们,这是只读的。C#和javascript告诉咱们,这是可读写的。C++告诉咱们,大家本身来决定每个符号的规则。做为一个对语言了解得很深入,知道本身每一行代码到底在作什么,并且还颇有自制力的程序员来讲,我仍是比较喜欢C#那种作法。由于其实C++就算你把一个值抓了下来,大部分状况下仍是不能优化的,那何苦每一个变量都要我本身说明我究竟是想只读呢,仍是要读写均可以呢?函数体我怎么用这个变量不是已经很清楚的表达出来了嘛。

那说到底闭包是什么呢?闭包其实就是那个被lambda表达式抓下来的“上下文”加上函数自己了。像上面的SetX函数里面的lambda表达式的闭包,就是x变量。一个语言有了带闭包的lambda表达式,意味着什么呢?我下面给你们展现一小段代码。如今要从动态类型的的lambda表达式开始讲,就凑合着用那个无聊的javascript吧:

function pair(a, b) {
    return function(c) {
        return c(a, b);
    };
}

function first(a, b) {
    return a;
}

function second(a, b) {
    return b;
}

var p = pair(1, pair(2, 3));
var a = p(first);
var b = p(second)(first);
var c = p(second)(second);
print(a, b, c);

这个程序的a、b和c究竟是什么值呢?固然就算看不懂这个程序的人也能够很快猜出来他们是一、2和3了,由于变量名实在是定义的太清楚了。那么程序的运行过程究竟是怎么样的呢?你们能够看到这个程序的任何一个值在建立以后都没有被第二次赋值过,因而这种程序就是没有反作用的,那就表明其实在这里call by value和call by need是没有区别的。call by need意味着函数的参数的求值顺序也是无所谓的。在这种状况下,程序就变得跟数学公式同样,能够推导了。那咱们如今就来推导一下:

var p = pair(1, pair(2, 3));
var a = p(first);

// ↓↓↓↓↓

var p = function(c) {
    return c(1, pair(2, 3));
};
var a = p(first);

// ↓↓↓↓↓

var a = first(1, pair(2, 3));

// ↓↓↓↓↓

var a = 1;

这也算是个老掉牙的例子了啊。闭包在这里体现了他强大的做用,把参数保留了起来,咱们能够在这以后进行访问。仿佛咱们写的就是下面这样的代码:

var p = {
    first : 1,
    second : {
        first : 1,
        second : 2,
    }
};

var a = p.first;
var b = p.second.first;
var c = p.second.second;

因而咱们获得了一个结论,(带闭包的)lambda表达式能够代替一个成员为只读的struct了。那么,成员能够读写的struct要怎么作呢?作法固然跟上面的不同。究其缘由,就是由于javascript使用了call by value的规则,使得pair里面的return c(a, b);没办法将a和b的引用传递给c,这样就没有人能够修改a和b的值了。虽然a和b在那些c里面是改不了的,可是pair函数内部是能够修改的。若是咱们要坚持只是用lambda表达式的话,就得要求c把修改后的全部“这个struct的成员变量”都拿出来。因而就有了下面的代码:

// 在这里咱们继续使用上面的pair、first和second函数

function mutable_pair(a, b) {
    return function(c) {
        var x = c(a, b);
        // 这里咱们把pair当链表用,一个(1, 2, 3)的链表会被储存为pair(1, pair(2, pair(3, null)))
        a = x(second)(first);
        b = x(second)(second)(first);
        return x(first);
    };
}

function get_first(a, b) {
    return pair(a, pair(a, pair(b, null)));
}

function get_second(a, b) {
    return pair(b, pair(a, pair(b, null)));
}

function set_first(value) {
    return function(a, b) {
        return pair(undefined, pair(value, pair(b, null)));
    };
}

function set_second(value) {
    return function(a, b) {
        return pair(undefined, pair(a, pair(value, null)));
    };
}

var p = mutable_pair(1, 2);
var a = p(get_first);
var b = p(get_second);
print(a, b);
p(set_first(3));
p(set_second(4));
var c = p(get_first);
var d = p(get_second);
print(c, d);

咱们能够看到,由于get_first和get_second作了一个只读的事情,因此返回的链表的第二个值(表明新的a)和第三个值(表明新的b)都是旧的a和b。可是set_first和set_second就不同了。所以在执行到第二个print的时候,咱们能够看到p的两个值已经被更改为了3和4。

虽然这里已经涉及到了“绑定过的变量从新赋值”的事情,不过咱们仍是能够尝试推导一下,究竟p(set_first(3));的时候究竟干了什么事情:

var p = mutable_pair(1, 2);
p(set_first(3));

// ↓↓↓↓↓

p = return function(c) {
    var x = c(1, 2);
    a = x(second)(first);
    b = x(second)(second)(first);
    return x(first);
};
p(set_first(3));

// ↓↓↓↓↓

var x = set_first(3)(1, 2);
p.a = x(second)(first); // 这里的a和b是p的闭包内包含的上下文的变量了,因此这么写会清楚一点
p.b = x(second)(second)(first);
// return x(first);出来的值没人要,因此省略掉。
// ↓↓↓↓↓

var x = (function(a, b) {
    return pair(undefined, pair(3, pair(b, null)));
})(1, 2);
p.a = x(second)(first);
p.b = x(second)(second)(first);// ↓↓↓↓↓

x = pair(undefined, pair(3, pair(2, null)));
p.a = x(second)(first);
p.b = x(second)(second)(first);// ↓↓↓↓↓

p.a = 3;
p.b = 2;

因为涉及到了上下文的修改,这个推导严格上来讲已经不能叫推导了,只能叫解说了。不过咱们能够发现,仅仅使用能够捕捉可读写的上下文的lambda表达式,已经能够实现可读写的struct的效果了。并且这个struct的读写是经过getter和setter来实现的,因而只要咱们写的复杂一点,咱们就获得了一个interface。因而那个mutable_pair,就能够当作是一个构造函数了。

大括号不能换行的代码真他妈的难读啊,远远望去就像一坨屎!go语言还把javascript自动补全分号的算法给抄去了,真是没品位。

因此,interface其实跟lambda表达是同样,也能够当作是一个闭包。只是interface的入口比较多,lambda表达式的入口只有一个(相似于C++的operator())。你们可能会问,class是什么呢?class固然是interface内部不可告人的实现细节的。咱们知道,依赖实现细节来编程是不对的,因此咱们要依赖接口编程

固然,即便是仓促设计出javascript的那我的,大概也是知道构造函数也是一个函数的,并且类的成员跟函数的上下文链表的节点对象其实没什么区别。因而咱们会看到,javascript里面是这么作面向对象的事情的:

function rectangle(a, b) {
    this.width = a;
    this.height = height;
}

rectangle.prototype.get_area = function() {
    return this.width * this.height;
};

var r = new rectangle(3, 4);
print(r.get_area());

而后咱们就拿到了一个3×4的长方形的面积12了。不过javascript给咱们带来的一点点小困惑是,函数的this参数实际上是dynamic scoping的,也就是说,这个this究竟是什么,要看你在哪如何调用这个函数。因而其实

obj.method(args)

整个东西是一个语法,它表明method的this参数是obj,剩下的参数是args。惋惜的是,这个语法并非由“obj.member”和“func(args)”组成的。那么在上面的例子中,若是咱们把代码改成:

var x = r.get_area;
print(x());

结果是什么呢?反正不是12。若是你在C#里面作这个事情,效果就跟javascript不同了。若是咱们有下面的代码:

class Rectangle
{
    public int width;
    public int height;

    public int GetArea()
    {
        return width * height;
    }
};

那么下面两段代码的意思是同样的:

var r = new Rectangle
{
    width = 3;
    height = 4;
};

// 第一段代码
Console.WriteLine(r.GetArea());

// 第二段代码
Func<int> x = r.GetArea;
Console.WriteLine(x());

究其缘由,是由于javascript把obj.method(a, b)解释成了GetMember(obj, “method”).Invoke(a, b, this = r);了。因此你作r.get_area的时候,你拿到的实际上是定义在rectangle.prototype里面的那个东西。可是C#作的事情不同,C#的第二段代码其实至关于:

Func<int> x = ()=>
{
    return r.GetArea();
};
Console.WriteLine(x());

因此说C#这个作法比较符合直觉啊,为何dynamic scoping(譬如javascript的this参数)和call by name(譬如C语言的宏)看起来都那么屌丝,老是让人掉坑里,就是由于违反了直觉。不过javascript那么作仍是情有可原的。估计第一次设计这个东西的时候,收到了静态类型语言太多的影响,因而把obj.method(args)整个当成了一个总体来看。由于在C++里面,this的确就是一个参数,只是她不能让你obj.method,得写&TObj::method,而后还有一个专门填this参数的语法——没错,就是.*和->*操做符了。

假如说,javascript的this参数要作成lexical scoping,而不是dynamic scoping,那么能不能用lambda表达式来模拟interface呢?这固然是能够,只是若是不用prototype的话,那咱们就会丧失javascript爱好者们想方设法绞尽脑汁用尽奇技淫巧锁模拟出来的“继承”效果了:

function mutable_pair(a, b) {
    _this = {
        get_first = function() { return a; },
        get_second = function() { return b; },
        set_first = function(value) { a = value; },
        set_second = function(value) { b = value; }
    };
return _this; } var p = new mutable_pair(1, 2); var a = p.get_first(); var b = p.get_second(); print(a, b); var c = p.set_first(3); var d = p.set_second(4); print(c, d);

这个时候,即便你写

var x = p.set_first;
var y = p.set_second;
x(3);
y(4);

代码也会跟咱们所指望的同样正常工做了。并且创造出来的r,全部的成员变量都屏蔽掉了,只留下了几个函数给你。与此同时,函数里面访问_this也会获得建立出来的那个interface了。

你们到这里大概已经明白闭包、lambda表达式和interface之间的关系了吧。我看了一下以前写过的六篇文章,加上今天这篇,内容已经覆盖了有:

  1. 阅读C语言的复杂的声明语法
  2. 什么是语法噪音
  3. 什么是语法的一致性
  4. C++的const的意思
  5. C#的struct和property的问题
  6. C++的多重继承
  7. 封装到底意味着什么
  8. 为何exception要比error code写起来干净、容易维护并且不须要太多的沟通
  9. 为何C#的有些interface应该表达为concept
  10. 模板和模板元编程
  11. 协变和逆变
  12. type rich programming
  13. OO的消息发送的含义
  14. 虚函数表是如何实现的
  15. 什么是OO里面的类型扩展开放/封闭与逻辑扩展开放/封闭
  16. visitor模式如何逆转类型和逻辑的扩展和封闭
  17. CPS(continuation passing style)变换与异步调用的异常处理的关系
  18. CPS如何让exception变成error code
  19. argument passing和symbol resolving
  20. 如何用lambda实现mutable struct和immutable struct
  21. 如何用lambda实现interface

想了想,大概通俗易懂的能够自学成才的那些东西大概都讲完了。固然,系列是不会在这里就结束的,只是后面的东西,大概就须要你们多一点思考了。

写程序讲究行云流水。只有本身勤于思考,勤于作实验,勤于造轮子,才能让编程的学习事半功倍。

相关文章
相关标签/搜索