Scheme语言中,可以使用call/cc,
得到当前表达式的Continuation。
例如:
(+ 1 (call/cc (lambda (k)
(* 2 (k 3)))))
=> 4
CPS,是Continuation Passing Style的缩写,
它是一种编码风格,
函数执行完以后,并不通过返回值,
而是调用它自己的Continuation来完成计算。
例如,
函数通过返回值的方式,通常这样写,
(define (add x y)
(+ x y))
(display (add 1 2))
=> 3
改写成CPS方式,
(define (add x y k)
(k (+ x y)))
(add 1 2 display)
=> 3
其中display这个单参函数是表达式add的Continuation,
在调用add函数的时候,我们把它的Continuation当做附加参数传递过去了,
等add函数体的计算(+ x y)执行完后,
并没有通过返回值,而是直接调用了它的Continuation——k。
CPS的误区
CPS是一种代码风格,
它可以用来做任何适当的事情,
并不是为了call/cc而出现的。
CPS可以显式的指明程序的计算过程,
在函数式语言的编译器中,常用来作为代码的中间表示(IR)。
例如:Standard ML of New Jersey编译器,大致有以下几个处理过程。
(1)词法分析,语法分析,类型检查,建立抽象语法树
(2)转换成lambda演算的某种表示
(3)对用lambda演算表示的程序进行优化
(4)转换成CPS的某种表示
(5)优化CPS
(6)闭包变换(closure conversion),消除函数的自由变量
(7)去除嵌套作用域,得到一系列无嵌套的相互递归函数
(8)处理寄存器溢出,每个函数至多含有n个变量,n是目标机器寄存器数
(9)生成目标机器指令
CPS与尾递归,也没有必然的联系。
虽然很多地方都喜欢通过把函数转换成尾递归化来介绍CPS。
转换成CPS
有关CPS的研究很多,
将任意函数转换成CPS,
已经不是困难的事情了。
我们不做形式化的探讨,
只手动处理,借几个例子来体会一下。
例一:含有自由变量的函数
(define y 1)
(define (f x)
(+ x y))
(display (f 2))
=> 3
我们看到f的函数体中(+ x y),y是自由的,
它没有在函数体中定义。
根据词法作用域规则,
y,应该去定义f的环境中查找。
得到y => 1。
我们通过为f增加附加参数k的方法, 将具有返回值的函数转换成CPS。
(define y 1)
(define (f x k)
(k (+ x y)))
(f 2 display)
=> 3
例二:阶乘函数
(define (fact n)
(if (<= n 2)
n
(* (fact (- n 1)) n)))
(display (fact 5))
=> 120
fact函数是递归的,为了转换成CPS,在增加附加参数k时候,
所有fact调用的地方都要修改,(define (fact n k)
难以修改的地方,可以思考一下实际的计算顺序,作为方向。
(if (<= n 2)
n
这里,返回n后要执行k所示的剩余计算,
相当于(k n)。
(* (fact (- n 1)) n)
考虑运算顺序,先执行(fact (- n 1)),
增加附加参数以后,变成了(fact (- n 1) ?),
其中“?”表示该表达式后续的剩余计算,是什么呢,
考虑计算顺序,如果计算完成,则要先乘以n,在执行k所示的剩余计算。
所以,
? =>
(lambda (v)
(k (* v n)))
整理得,
(define (fact n k)
(if (<= n 2)
(k n)
(fact (- n 1)
(lambda (v)
(k (* v n))))))
(fact 5 display)
=> 120
例三:斐波那契函数
(define (fib n)
(if (<= n 2)
1
(+ (fib (- n 1)) (fib (- n 2)))))
(display (fib 5))
=> 5
先为函数增加附加参数k,
(define (fib n k)
再改变非递归情况的返回值方式,
(if (<= n 2)
1
(if (<= n 2)
(k 1)
然后考虑(+ (fib (- n 1)) (fib (- n 2)))的运算顺序,
是从计算(fib (- n 1))开始的,
增加附加参数后,改写为,(fib (- n 1) ?)
“?”是什么呢,它表示(fib (- n 1))表达式后续的计算:
先计算(fib (- n 2)),再把结果值相加,最后执行k所示的计算。
? =>
(lambda (v1)
(fib (- n 2) ??))
=>
(lambda (v1)
(fib (- n 2)
(lambda (v2)
(k (+ v1 v2)))))
整理得,
(define (fib n k)
(if (<= n 2)
(k 1)
(fib (- n 1)
(lambda (v1)
(fib (- n 2)
(lambda (v2)
(k (+ v1 v2))))))))
(fib 5 display)
=> 5
总结一下,
函数调用的返回值变成了,Continuation的参数,
然后在Continuation中处理后续的计算。
(fn x)
=> (fn x (lambda (v) ...))
其中v是原来(fn x)的返回值,
(fn x)的后续计算,写在“...”处。
闭包变换
限于目前大多数计算机的体系结构,
函数求值时,并不直接支持对词法变量的查找规则。
我们可以继续改变CPS,
使得每个函数都不包含自由变量。
这种改变方式,称为闭包变换,
这种代码风格,称为Closure Passing Style。
对于任意含有自由变量的函数,我们都可以进行闭包变换,
并不一定是已经表示成CPS风格的程序,
只是CPS变换与闭包变换有些相似之处。
我们知道,
词法作用域规则,是当我们查找函数中自由变量的时候,
到定义该函数的环境中去查找。
为了在运行时得到函数定义时的环境,
我们需要再增加一个或几个参数,来表示函数定义时的环境。
为了连贯性,我们直接使用上文CPS转换后的结果,作为下一步闭包变换的起点。
例一:含有自由变量的函数
(define y 1)
(define (f x k)
(k (+ x y)))
(f 2 display)
=> 3
我们再增加一个参数e,来表示函数定义时的环境。
(define y 1)
(define (f x k e)
(define y (vector-ref e 0))
(k (+ x y)))
(define e0 (vector y f))
(f 2 display e0)
=> 3
注:
(vector y f)),用vector表示环境,y是第0个元素,f是第1个元素。
(vector-ref e 0)),取环境中第0个元素,y
例二:阶乘函数
(define (fact n k)
(if (<= n 2)
(k n)
(fact (- n 1)
(lambda (v)
(k (* v n))))))
(fact 5 display)
=> 120
我们先将Continuation中的lambda表达式写成定义式,便于理解,
(define (fact n k)
(define (k1 v)
(k (* v n)))
(if (<= n 2)
(k n)
(fact (- n 1) k1)))
我们看到,在Continuation函数中也包含了自由变量,
所以,情况比较复杂,
我们需要再传递一个参数,来表示Continuation定义时的环境,
用来区分fact函数定义时的环境。
(define (k0 v e)
(display v))
(define (fact n k fact-e k-e)
(define (k1 v e)
(define n (vector-ref e 0))
(define k (vector-ref e 1))
(define k-e (vector-ref e 3))
(k (* v n) k-e))
(define fact (vector-ref fact-e 1))
(define k1-e (vector n k fact-e k-e k1 fact))
(if (<= n 2)
(k n k-e)
(fact (- n 1) k1 fact-e k1-e)))
(define fact-e0 (vector k0 fact))
(define k-e0 fact-e0)
(fact 5 k0 fact-e0 k-e0)
=> 120
关键点在于,从定义时的环境中得到词法变量。
例三:斐波那契函数
(define (fib n k)
(if (<= n 2)
(k 1)
(fib (- n 1)
(lambda (v1)
(fib (- n 2)
(lambda (v2)
(k (+ v1 v2))))))))
(fib 5 display)
=> 5
先将Continuation中的lambda表达式写成定义式
(define (fib n k)
(define (k1 v1)
(define (k2 v2)
(k (+ v1 v2)))
(fib (- n 2) k2))
(if (<= n 2)
(k 1)
(fib (- n 1) k1)))
将函数定义时的环境,当做附加参数传递,
(define (k0 v fib-e k-e)
(display v))
(define (fib n k fib-e k-e)
(define (k1 v1 fib-e k-e)
(define (k2 v2 fib-e k-e)
(define k (vector-ref (vector-ref k-e 2) 1))
(define v1 (vector-ref k-e 0))
(define k-k-e (vector-ref (vector-ref k-e 2) 3))
(k (+ v1 v2) fib-e k-k-e))
(define fib (vector-ref fib-e 1))
(define n (vector-ref k-e 0))
(define k2-e (vector v1 fib-e k-e k2 fib n))
(fib (- n 2) k2 fib-e k2-e))
(define fib (vector-ref fib-e 1))
(define k1-e (vector n k fib-e k-e k1 fib))
(if (<= n 2)
(k 1 fib-e k-e)
(fib (- n 1) k1 fib-e k1-e)))
(define fib-e0 (vector k0 fib))
(define k-e0 fib-e0)
(fib 5 k0 fib-e0 k-e0)
=> 5
至此,三个例子中,所有的函数都没有自由变量了。
结语
本文只是手动处理了CPS变换和闭包变换。
CPS的用处还不止于此。
然而,我认为,重要的是这种附加参数的思想。
学会了思想,我们才能灵活运用它。
发现更神奇的计算本质。
本文参考:《Compiling with Continuations》