理解call/cc
第一次见call/cc,顿时懵逼。百思不得其解。
后来想明白了,发现大多数网上的讲解有误。属于生拿结果套解法,看起来没有错,但是顺着这个错误的思路,就死活理解不了。
比如以下的程序:
(+
(*
(call/cc
(lambda (c)
(+
(c 2)
3)))
5)
8)
一般网上的讲解是这样的,call/cc内部的lambda参数c一旦调用,就忽略掉下一条语句(+ 3),直接返回到call/cc处,所以等价于代码
(+
(* 2 5)
8)
得出18。
看起来一切都很美好,代码跑起来也符合18的结果。但是,让你接着理解,使用call/cc,就懵逼了吧。因为跟着错误的思路,你根本无法理解它的用处。
比如 (set! *this* c)
,这是什么鬼。
以下是正确理解的思路:
call/cc表达式,提取当前位置的延续。
那么,上一个例子,可以先简化为
(+
(*
(call/cc …)
5)
8)
什么叫当前位置的延续呢?
比如
(+
(* x 5)
8)
对于x来说,它的延续就是它将要被做的事。这里就是,*5然后+8。
这个延续如何用代码表示呢?
(lambda (k)
(+
(* k 5)
8))
那么对于x 也就是(call/cc …)
的延续就是
(lambda (k)
(+
(* k 5)
8))
现在来看(call/cc ...)语句内部,当call/cc取得当前位置的延续后,它需要一个回调函数来使用这个延续。对吧?因为从这里控制流开始向回调函数处跳转,而不是返回到(call/cc ...)所在的当前栈。
也就是说,(+ (* (call/cc …)5) 8)
的加法和乘法是永远也不会执行的!从call/cc开始,就走了,走了,永远也不回来了。去哪了,去(call/cc f)的回调函数f那里了。
这就是网上通常解释的错误之处,解释成(call/cc ...)表达式的结果,去执行(+ ( x 5) 8)了。因为这里有一个陷阱,(call/cc ...)在这里太容易看成是求值结果得到x,然后值x被带入(+ ( x 5) 8)求值了。因为这是call/cc于scheme例程所格格不入的地方。
然后来看回调函数f:
(lambda (c)
(+
(c 2)
3))
f被以call/cc得到的延续为参数调用,同时这个延续也作为f的回调函数,请注意后一点。
参数c在这里带入
(lambda (k)
(+
(* k 5)
8))
那么回调函数f在这里的求值就是:
((lambda (c)
(+
(c 2)
3))
(lambda (k)
(+
(* k 5)
8)))
注意 这里的求值是永远不会被返回的。(普通scheme程序这里会得到21)
因为回调函数f,在求值时将控制流传递给了它的回调函数c,如果使用有return的语言来表达的话。
var f = function(c){
var x = c(2);
return x;
x+3;
}
x+3是永远不会执行的。
这样的返回方式是call/cc的特殊之处,正是这样,它才能暴露出整个程序的控制流。
所以整个程序的求值返回了
((lambda (c)
(c 2))
(lambda (k)
(+
(* k 5)
8)))
的结果。
注意这个结果和(+ (* x 5) 8)
这个外层函数一点关系都没有,它已经成为过去了。
那么现在我们就很好理解
(set! *this* c)
(*this* 10)
这样的写法了。延续c只不过被储存了起来,随时调用罢了。而这个c在当下与过去的外层函数一点关系没有了。
(+
(*
(call/cc
(lambda (c)
(set! *this* c)
1))
5)
8)
在这个例子里,因为回调函数也就是延续c一直没有被调用。
那么call/cc的回调函数会返回1,结果是13。
但这里我们把延续存起来了,于是:
(*this* 2)
相当于
((lambda (c)
(c 2))
(lambda (k)
(+
(* k 5)
8)))
得到 18
(*this* 3)
相当于
((lambda (c)
(c 3))
(lambda (k)
(+
(* k 5)
8)))
得到 23
可以看到这里的 this 等于c 和原函数 (+ (* x 5) 8)
一点关系没有了。
© 2019-2020 guenchi. All rights reserved.