我又从你的未来世界回来。

开门见山

1
2
3
4
5
(let* ((yin
((lambda (cc) (display #\@) cc) (call-with-current-continuation (lambda (c) c))))
(yang
((lambda (cc) (display #\*) cc) (call-with-current-continuation (lambda (c) c)))))
(yin yang))

这段代码的输出是什么呢?如果你的回答是

1
@*@**@***@****@...

那么你真的很厉害啊。下面的废话就不用看了。到此为止。

如果你的回答不是那样,那么也没关系,毕竟这只是一段代码,不理解也不影响明天太阳升起。

如果你执意要理解,那么请先学习 Scheme 的基本语法之后再来挑战。Bye~

立入禁止

好吧。如果你在没有相关基础的情况下还要尝试的话很可能看不懂,毕竟我只是抱着玩的心态随便讲讲啊。看不懂才是自然的,看得懂就逆天了。

坑爹提示:以下内容配合 CPSCall/cc 食用更佳哦。

无限轮回

先讲一下 Scheme 的 let* 变量绑定。这个比较简单,看一个例子就明白了。

1
2
3
4
(let* ((a 1)           ; a = 1
(b (+ a 1))) ; b = a + 1 = 2
(+ a b)) ; a + b = 1 + 2 = 3
; => 3

阴阳谜题的代码结构类似,忽略细节是这样的

1
2
3
4
5
(let* ((yin            ; yin = xxx
; ... ; 你会回来看的
(yang ; yang = yyy
; ... ; 你还会回来看的
(yin yang))

接着看 yinyang 赋了什么值。

1
2
3
4
5
(let* ((yin
((lambda (cc) (display #\@) cc) (call-with-current-continuation (lambda (c) c))))
(yang
((lambda (cc) (display #\*) cc) (call-with-current-continuation (lambda (c) c)))))
(yin yang))

你可能会觉得这一长串是什么鬼。别急慢慢来。首先单独拎出来

1
((lambda (cc) (display #\@) cc) (call-with-current-continuation (lambda (c) c))) ; yin 的赋值

这个 lambda 是 Scheme 定义匿名函数的方式。

1
2
3
4
5
6
7
8
9
10
11
;(lambda (参数列表) 
; (函数体))
; 定义匿名函数 function (x) {return x+1;}
(lambda (x)
(+ x 1)) ; Scheme 返回值为最后一个表达式的计算结果。
;; 给匿名函数传递参数
;((lambda (参数列表)
; (函数体)) 参数)
; 给匿名函数 (function(x) {return x+1;})(1) 传递参数 1
((lambda (x)
(+ x 1)) 1)

回到之前的代码,重新排版一下

1
2
3
((lambda (cc) 
(display #\@)
cc) (call-with-current-continuation (lambda (c) c)))

所以这个函数做了两件事。一是显示 @ ,而是原封不动地返回接收的参数 cc

这个匿名函数还接收了参数,参数是

1
(call-with-current-continuation (lambda (c) c))

这个 call-with-current-continuation 是 Scheme 的一个特殊函数,返回一个 continuation 。简称为 Call/cc

详情请移步 Call/cc

Call/cc 给匿名函数传了一个 continuation 。

这个匿名函数的作用是原封不动的返回接收到的参数。

所以这句话返回一个 continuation 。

如果你还没有看上述链接,可以简单地认为这里做了个标记。使用这个标记,相当于 goto 回到这句话执行结束的位置。

只有调用 Call/cc 才会产生新的 continuation 。调用 continuation 导致 Call/cc 返回,不产生新的 continuation。

为了描述方便,在 Call/cc 处分别做标记。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(let* ((yin
((lambda (cc)
(display #\@) ; 输出 @
cc) (call-with-current-continuation (lambda (c) c)))) ;; 生成 cc0 因为调用了 Call/cc
; 输出为 @
; yin = cc0
; yang = ???
; 继续往下执行
(yang
((lambda (cc)
(display #\*) ; 输出 *
cc) (call-with-current-continuation (lambda (c) c))))) ;; 生成 cc1 因为调用了 Call/cc
; 输出为 @*
; yin = cc0 <- 上面赋值为 cc0
; yang = cc1
(yin yang)) ; (cc0 cc1)

这是一开始的赋值情况。最后执行 (yin yang) 代入值为 (cc0 cc1)

这是什么意思呢。

上面说过了,调用标记相当于 goto 回到标记处,这个 goto 比较特殊可以把接收的参数带回去。

所以回到 cc0 处也就是

1
2
3
4
5
6
(let* ((yin
((lambda (cc) ; 接收的参数是 Call/cc 返回的 cc1
(display #\@)
cc) (call-with-current-continuation (lambda (c) c)))) ;; 这里是 cc0 。Call/cc 返回 cc1
;...
)

然后 Call/cc 接收到 cc1 ,返回 cc1 ,传给匿名函数。

这里需要注意的是给匿名函数传递参数首先要计算参数的值,也就是先计算 Call/cc ,得到返回值后再计算匿名函数。

所以相当于 goto 回到了赋值开始的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(let* ((yin
((lambda (cc) ; 接收的参数是 Call/cc 返回的 cc1
(display #\@) ; 输出 @
cc) (call-with-current-continuation (lambda (c) c)))) ;; 这里是 cc0 。Call/cc 返回 cc1
; 输出为 @*@ <- 之前已经输出了 @*
; yin = cc1 <- 这里赋值为匿名函数的返回值 cc1
; yang = ???
; 继续往下执行
(yang
((lambda (cc)
(display #\*) ; 输出 *
cc) (call-with-current-continuation (lambda (c) c))))) ;; cc2 因为调用了 Call/cc
; 输出为 @*@*
; yin = cc1 <- 上面赋值为 cc1
; yang = cc2
(yin yang)) ; (cc1 cc2)

同样地,调用 (cc1 cc2) 会将 cc2 带到 cc1 处。cc1 处在哪呢?在上面给 yang 赋值的位置。

1
2
3
4
5
6
7
8
9
10
(let* ((yin
;...
(yang
((lambda (cc) ; 接收的参数是 Call/cc 返回的 cc2
(display #\*) ; 输出 *
cc) (call-with-current-continuation (lambda (c) c))))) ;; 这里是 cc1 。Call/cc 返回 cc2
; 输出为 @*@** <- 上面已经输出 @*@*
; yin = cc0 <- cc1 处的 yin 为 cc0
; yang = cc2 <- 命运改写了,这次 yang 被赋值为 cc2
(yin yang)) ; (cc0 cc2)

同样地,调用 (cc0 cc2) 会将 cc2 带到 cc0 处。cc0 处在哪呢?在上面给 yin 赋值的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(let* ((yin
((lambda (cc) ; 接收的参数是 Call/cc 返回的 cc2
(display #\@) ; 输出 @
cc) (call-with-current-continuation (lambda (c) c)))) ;; 这里是 cc0 。Call/cc 返回 cc2
; 输出为 @*@**@ <- 上面已经输出 @*@**
; yin = cc2 <- 命运改写了,这次 yin 被赋值为 cc2
; yang = ???
; 继续往下执行
(yang
((lambda (cc)
(display #\*) ; 输出 *
cc) (call-with-current-continuation (lambda (c) c))))) ;; cc3 因为调用了 Call/cc
; 输出为 @*@**@*
; yin = cc2 <- 上面赋值为 cc2
; yang = cc3
(yin yang)) ; (cc2 cc3)

同样地,调用 (cc2 cc3) 会将 cc3 带到 cc2 处。cc2 处在哪呢?在上面给 yang 赋值的位置。

1
2
3
4
5
6
7
8
9
10
(let* ((yin
; ...
(yang
((lambda (cc) ; 接收的参数是 Call/cc 返回的 cc3
(display #\*) ; 输出 *
cc) (call-with-current-continuation (lambda (c) c))))) ;; 这里是 cc2 。Call/cc 返回 cc3
; 输出为 @*@**@** <- 上面已经输出 @*@**@*
; yin = cc1 <- cc2 处的 yin 为 cc1
; yang = cc3 <- 命运改写了,这次 yang 被赋值为 cc3
(yin yang)) ; (cc1 cc3)

同样地,调用 (cc1 cc3) 会将 cc3 带到 cc1 处。cc1 处在哪呢?在上面给 yang 赋值的位置。

1
2
3
4
5
6
7
8
9
10
(let* ((yin
;...
(yang
((lambda (cc) ; 接收的参数是 Call/cc 返回的 cc3
(display #\*) ; 输出 *
cc) (call-with-current-continuation (lambda (c) c))))) ;; 这里是 cc1 。Call/cc 返回 cc3
; 输出为 @*@**@*** <- 上面已经输出 @*@**@**
; yin = cc0 <- cc1 处的 yin 为 cc0
; yang = cc3 <- 命运改写了,这次 yang 被赋值为 cc3
(yin yang)) ; (cc0 cc3)

同样地,调用 (cc0 cc3) 会将 cc3 带到 cc0 处。cc0 处在哪呢?在上面给 yin 赋值的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(let* ((yin
((lambda (cc) ; 接收的参数是 Call/cc 返回的 cc3
(display #\@) ; 输出 @
cc) (call-with-current-continuation (lambda (c) c)))) ;; 这里是 cc0 。Call/cc 返回 cc3
; 输出为 @*@**@***@ <- 上面已经输出 @*@**@***
; yin = cc3 <- 命运改写了,这次 yin 被赋值为 cc3
; yang = ???
; 继续往下执行
(yang
((lambda (cc)
(display #\*) ; 输出 *
cc) (call-with-current-continuation (lambda (c) c))))) ;; cc4 因为调用了 Call/cc
; 输出为 @*@**@***@*
; yin = cc3 <- 上面赋值为 cc3
; yang = cc4
(yin yang)) ; (cc3 cc4)

同样地,调用 (cc3 cc4) ……

会一直按上述规律一直无限循环下去。

最后

没想到有一天会写阴阳谜题,因为相关的前置基础太多了,写完得多累。但是写了 CPS 就自然写 Call/cc ,写了 Call/cc 写阴阳谜题就是水到渠成的了。再重申一次,我只是抱着玩的心态随便讲讲啊。看不懂才是自然的,看得懂就逆天了(逃