CPS变换的简单理解

来源:互联网 发布:java游戏圈钱 编辑:程序博客网 时间:2024/05/16 00:52

CPS变换又叫做continuation Passing Style,它是一种编程风格,用来将内部要执行的逻辑封装到一个闭包里面,然后再返回给调用者,这就将它的程序流程显式的暴露给了程序员,让我们可以控制它。

我们先看一个cps变换的小例子:

import tracebackdef disp(x):    if x>0:disp(x-1)    else:print((len(traceback.extract_stack()),x))def test(k,n):    #return test(k,n-1) if n>0 else k(42)    if n>0:        x =  test(k,n-1)        return x    else:return k(42)#注意最后这个(),因为f最后返回一个函数c(42) = def f() :disp(42),所以要调用一下f()C = lambda f: lambda c, *a: f(lambda x: lambda : c(x), *a)()C(test)(disp, 990)#test(disp,990) stack overflow

我们将disp这个函数要做的事情返回给了程序的调用者(caller),这样我们就可以在调用disp这个逻辑的时候,已经从程序的栈里面出来了,这样就可以避免栈的溢出。

上面的代码可以看作我们人肉的改变了disp的运行流程,将这个逻辑封装在一个闭包里面,然后决定什么时候调用。

cps还有其他很多用途,这里我只关注用户代码方面的…一个小例子,如果我们需要实现协程,需要挂起某一个协程,然后需要的时候再执行这个协程。这样我们就需要将要挂起之后执行的逻辑包装起成一个continuation,当需要的时候再执行这个continuation。

[2]continuation对尾递归的优化:这一块感觉有一点难以理解,反正我是看了很久才有一点懂,大概说一下理解吧。首先python,c++这种都是不支持尾递归优化的,所以如果cps变换在这里来讲作用不是很大。但是多了解一点总是好的。对于函数式来说,尾递归就很重要了,因为没有循环可以用。但是有些不好写成尾递归,比如二叉树先序遍历,在函数式里面就很难写成尾递归,这里就要用到continuation。而continuation则可以将本来在栈里面的调用被我们强行赛入一大堆lambda函数里面(利用闭包捕获调用的上下文)

具体怎么理解呢,首先continuation看作是接下来要做的工作,传入的continuation则代表上面的调用者希望继续完成的工作,利用当前的一层包装将当前的工作塞入新的continuation里面就完成了继续要做的工作的抽象!然后传入下一个参数里面,我们看一个fib数列的理解,首先当前的上面传入一个continuation是要做的工作,我们自己要做的是求出fib(n-1)和fib(n-2)并且将这两个加起来。关键是怎么抽象成下一个continuation?这里我想了特别久,最后是这么理解的:

先考虑直接调用fib_cps(n-1,”new continuation”),这个框架是肯定的。然后考虑下一次调用直接到递归出口了,那么肯定就要调用continuation(n),这样就开始展开计算了。这个n传入的参数不就是调用者的continuation接受的参数吗?所以continuation可以确定这样一部分:lambda x: continuation….,然后我们接下来还要计算fib(n-2),这一部分怎么搞呢?可以这样写:fib_cps(n-2,lambda y:continuation(x+y)),这样组合起来就成了下面的代码.注意理解,y是fib_cps(n-2)的返回结果,我们将最终的展开结果相加这个工作放到了新的里面的continuation上面去做。展开到这一层x+y算是完成这个递归调用的工作了,剩下的展开就需要后面的展开了。

def fib_cps(n,k):    if n<2:return k(n)    #x是fib_cps(n-1)的返回结果,y是fib(n-2),是第二个continuation返回的结果    return fib_cps(n-1,lambda x:fib_cps(n-2,lambda y:k(x+y)))def _k(x):return xx = fib_cps(10,_k)print(x)
0 0