学 C 语言,拦路虎是指针。学 Haskell,拦路虎是单子(Monad)。学 Scheme,拦路虎是什么呢?初学者觉得是层峦叠嶂的括号,实际上是续延(Continuation)。

我曾用过 C 的指针(数据指针 + 函数指针)模拟过单子 [1],其实已经触及了续延。还记得那个 bar 函数吧,

Maybe bar(Maybe a, Maybe (*contuation)(void *thing))
{
        return a.thing ? contuation(a.thing) : (Maybe){.thing = Nothing};
}

contuation 指针指向一个函数。这个函数用于处理 bar 不能够确定或不知道如何处理的事情,这种事情对于 bar 而言,可称为一切来自未来的计算,此时,这个函数可以称为 bar 的续延。也就是说,bar 函数完成它能完成的计算之后,会将计算结果传递给 contuation。当然,这在 C 语言里是没法做,但是可以这样认为。

要做到「这样认为」,需要做一点脑力体操。

看下面的表达式,它基于 bar 函数输出结果的赋值表达式,但是我故意不给 bar 函数提供第 2 个参数,而是用一个 [] 来表示它:

Maybe b = bar(foo(Nothing), []);

倘若我说,这个表达式等效于 1 个单参数函数,你信么?

倘若你不信……我想不出来为啥你会不信。需要 1 个参数才能求值的计算过程,不就是一个单参数的函数么?

不信的话,那么就将 bar(foo(Nothing), []) 打开,即:

return foo(Nothing).thing ? [](foo(Nothing).thing) : (Maybe){.thing = Nothing};

看上去,是一些表达式包围着 []。从现在起,要认为 [] 表示一个洞,洞外的表达式掉入了这个洞,而后洞里发生了一些事,最后从这个洞里又掉出来一个东西。要达到这种认识,可能需要懂一点王阳明创立的心学,即「心外无物,心外无理」。你觉得是这些表达式包围了洞,但是没了这个洞,这些表达式是没意义的,等同于不存在。

倘若还是没开窍,那就拿一只杯子。将杯子的内部视为「外」,将杯子的外部视为「内」,那么这个杯子就包含了整个宇宙。倘若这个杯子不存在,那么宇宙也就没了意义,等同于不存在。接下来,你拿锤子敲掉这个杯子的底,你可以说这个宇宙从杯子的一个口掉了进去,又从另一个口掉了出来。

袖里乾坤大,壶中日月长。不吹,也不黑。

经过上述脑力体操之后,我宣称,bar 函数的返回值掉进了这个洞里,然后从这个洞里出来的东西作为值赋给了 b,应该很好理解。尽管它与我们的生活经验不太相符。

记住,带洞的表达式就是续延。可以认为,这个洞外面的东西会掉进这个洞里。不妨将这个洞称为「黑洞」。它对掉进来的东西作何处理,要看这个黑洞是什么样的黑洞了。

对于

bar(foo(Nothing), []);

下面这个函数是一个对掉进来的整型数乘以 10 的黑洞:

Maybe test(void *thing)
{
        (*(int *)thing) *= 10;
        return (Maybe){.thing = thing};
}

从这个黑洞里掉出来的值,最终被赋给 b,即:

Maybe b = bar(foo(Nothing), test);

再强调一下,在 C 语言里不是这样进行运算的,只是可以这样认为,而且只要你愿意,可以为 C 语言写一个这样的编译器。

现在,将 b 的赋值表达式的右半部分去掉,让它变成:

Maybe b = [];

这样,一个赋值表达式也能构成一个续延,因为它里面出现了黑洞。掉进黑洞里的 b(内存地址),会被黑洞与一个值(数据)绑定起来,然后它们从这个黑洞里掉出来。

基于续延里的黑洞,能够将多个续延串接起来,只需要将黑洞串接起来即可。例如,在 C 语言里,我要用 bar 组合一组相同类型的函数,可以这样做:

Maybe x = bar(bar(bar(foo(a), f), g), h);
注: 假设 a 已知,foo(a) 构造一个 Maybe 变量。

这个表达式有三重续延,它们像俄罗斯套娃一样。在 C 语言里,只能通过「看作是」的办法,认为整个表达式首先掉进了最内层的黑洞 f。从 f 里掉出来东西又掉进了第二层黑洞 g。从 g 里掉出来的东西又调入了第三层也就是最外层黑洞 h。从h 里掉出来的东西作为值赋给变量 x

将一个续延作为参数传给另一个续延,这就是所谓的续延传递,用这种办法可以形成一条有序的控制流(几个黑洞衔接起来)。用这种办法写程序,就是所谓的续延传递风格(Continuation-Passing Style,CPS)。

以上所述的东西,对于 C 的世界而言,是想象,但是对于 Scheme 的世界而言,就是客观存在。在 Scheme 的世界里,函数与续延,都是一等公民。函数可以作为参数传递给函数,续延可以作为参数传递给续延,函数可以作为参数传递给续延,续延可以作为参数传递给函数。

在 Scheme 里,假设也有一个 bar 函数,那么它将 fgh 这三个黑洞串接起来并将最后一个黑洞掉出来的结果赋值给一个变量,可用以下手法:

(define x
  (bar (foo a)
       (lambda (b)
         (bar (f b)
              (lambda (c)
                (bar (g c)
                     (lambda (d)
                       (foo d))))))))

值得注意的是,bar 函数是单子的核心部分,而 bar 函数可以视为一个续延,它的黑洞就是 contuation 指向的函数。如此说来,续延要比单子更基层。不过,单子也不是盖的,它也有办法规避 bar 这样的函数,而且使得自己依然是单子,并且它还能演绎出 bar 这样的函数。那么,将续延和单子看成一回事,如何?

在实践中,续延通常用来表达「等待」。

有诗云,有约不来过夜半,闲敲棋子落灯花。敲着棋子看灯花的人是一个续延,他掉进了爽约的人构成的黑洞里,从而失去了时间。

最后,来做一道哲学题:

先生(指王阳明)游南镇,一友指岩中花树问曰:「天下无心外之物,如此花树在深山中自开自落,于我心亦何相关?」先生曰:「你未看此花时,此花与汝心同归于寂;你来看此花时,此花颜色一时明白起来,便知此花不在你的心外。」

王阳明这一观点的错误是( )

A.把人对花的感觉与花的存在等同起来
B.把人对花的感觉与花的存在对立起来
C.主张人对花的感觉是主观与客观的统一
D.肯定人对花的感觉的能动性

这个题目,上过大学,学过马哲的人差不多都做过。王阳明龙场悟道,创立心学。他的成就,即使放到现代,能超越的人几乎不存在。结果,堂堂一代宗师,就被这么一个题目给毁了。

王阳明的观点没有错误,反正马克思没说他错了。说他这个观点错了的人,是学过马哲但没学过心学的人。大学不教心学,只教马哲,所以呢,全中国的大学生都会觉得王阳明太唯心,而且还是主观唯心……唯心就是封建迷信……至少我上学的时候,做了这个题目之后就是这么认为的。

观点本身没有对错。只是观点与受体可能会存在类型不匹配问题,从而触发了受体的异常机制。

有点跑题了。我要说的是续延。对于王阳明看花这个例子,可以理解为,花是一个续延,王阳明是它的黑洞,从这个黑洞里掉出来的结果是「王阳明心中的花」。王阳明的友人也是这朵花的黑洞,从这个黑洞里掉出来的结果是「王阳明友人心中的花」。那朵花本身没有变,只是掉入了不同的黑洞里时,输出的结果有所不同。

倘若全世界的人或者任何虫鱼禽兽都没看过那朵花,它会怎样?我不是很清楚。不过,倘若我是那朵花,我会尝试自己「看」自己——将自己作为续延,再将这个续延作为黑洞,结果得到的是「自我」。


[1] 单子,想弄不懂都很难

载入中...
garfileo garfileo

4.7k 声望

发布于专栏

while(1) { }

只关心 C 与 Lisp/Scheme 程序的基本形状,其他的事关心不过来。

136 人关注