递归——CPS(一)
程序中为什么需要栈stack?
普通的程序中,接触到子程序和函数的概念,很直观地,调用子程序时,会首先停止当前做的事情,转而执行被调用的子程序,等子程序执行完成后,再捡起之前挂起的程序,这有可能会使用刚才子程序计算出的数据。但是在程序被挂起的地方重新捡起程序并继续执行需要一个机制,即,存储当前所做事情的相关信息和以后在哪里捡起这个程序(现场信息)。这时,栈自然而然就是满足这种需要的一个数据结构(为什么使用栈略过不提)。
如果情况发生改变,没有函数需要返回,函数要么终止程序,要么是调用另一个函数,那么该怎么做?
很明显,治疗比疾病更糟糕。这种情况下,不必保持当前所做事情的跟踪信息,因为永远不会返回,那每次调用一个过程,这个过程取得所有控制权并且不再返回,该如何让所有工作能完成?
首先,写一个函数,然后在这个函数的最后调用另一个函数(尾调用)。控制权不再返回,所以在调用函数后,还有任何代码都是没有意义的。
其次,假设在调用函数 foo 后你还有其他更多的事情要做。Foo 现在跟你同一个位置,根据上面所说,调用函数的最后一件事要么终止程序,要么调用其他函数,因此,你需要将额外的工作放入一个函数bar,并且希望这些工作在foo之后执行,也就是说,确保foo调用bar而不是终止程序。
但是你没有写出foo,怎么知道foo在它完成时即将调用bar?你需要告诉foo,让foo调用bar,并且你希望foo能合作。(废话。。。)
第三,如果某个人调用了你的函数,那么需要做的最后一件事就一样了,因为你的函数不会返回。你需要寻找某种方式,来允许函数的调用者告诉你,在完成你的工作后还应该该做些什么。
这种函数的理念是永远不会返回,调用者传入它们的函数信息(这个函数信息指示下一步该做什么),这有点奇怪,但确是很强大的编程风格,这就是传说中的 Continuation Passing Style。Continuation 就是指 “下一步该做什么”的信息,它从一个函数传到另一个函数,因此得名。
很多语言天然支持CPS——Scheme, Ruby 和Rhino等。CPS 使得在我们正常的递归程序中不需要使用栈(来存储现场信息)。
下面将写出混合CPS和常规程序两种风格的代码,用JScript语言。
假设有如下JScript程序代码片段
function foo(x)
{
var y = bar();
blah(x, y);
}
function bar()
{
return 10;
}
function blah(a, b)
{
print(a + b);
}
foo(1);
这段代码很直观。那如何写CPS风格呢?首先,每个函数需要增加一个额外的参数来保持continuation。我们的程序调用foo然后终止,所以调用foo的continuation就是“终止程序”。假设现在就有一个魔法函数用来终止程序,则
function foo(x, cfoo)
{
// undone: rewrite foo in cps
}
foo(1, terminate);
再来理一下,调用foo 确保以下三件事顺序被完成:
1. 运行bar
2. 运行blah
3. 运行foo的调用者提供的continuation
但是blah不会返回,所以我们需要让blah在完成它自己的任务后再来调用 foo 的 continuation,于是重写blah如下
function(x, cfoo)
{
var y = bar();
blah(x, y, cfoo);
}
现在看起来是可以确保foo的continuation可以被调用从而终止程序,可是,bar也是要被设计为不返回的,所以按上面这个方式写,那是无法调用blah的。
还有一个问题是,bar将返回一个值,这个值是在后面的任务中用到的,但是我们已经移除了函数的返回值。解决方法是,让bar有一个continuation参数,然后将bar的返回值(这里指原先bar的返回值)作为这个continuation的参数。之前,你将返回值返回到你即将继续运行程序的地方,现在,你将返回值传入你下一步将要做的事情中,道理是相同的。
但是foo将传什么continuation给bar?嗯,当bar调用foo传给它的continuation时(这个continuation将应用一个参数,这个参数就是之前bar的返回值),foo是想干嘛?
这么一想就简单了,foo想使用之前bar的返回值和自己的参数来调用blah,于是重写函数如下:
function foo(x, afterfoo)
{
function foocontinuation(y)
{
blah(x, y, afterfoo);
}
}
function bar(afterbar)
{
afterbar(10);
}
function blah(a, b, afterblah)
{
print(a+b, afterblah);
}
function print(a, afterprint) // overload print
{
print(a);
afterprint();
}
foo(1, terminate);
foocontinuation是一个闭包(closure),所以它封装了x的值和afterfoo的信息,然后传入给bar。
总结过程如下:
- 程序传给foo的参数为 1 和 terminate
- foo传给bar的参数为 foocontinuation
- bar传给 foocontinuation 的参数为 10
- foocontinuation 给出 blah 的参数为 1,10 和 terminate
- blah 将参数相加得到11,然后将 11 和 terminate 传给 print
- print 打印 11,然后调用terminate,结束程序
实际中,JScript不知道这些函数都不返回值。JScript也不能聪明地意识到即使这些函数返回了值,这些函数在子程序调用完成后,也不再做任何事情,因此保持堆栈中旧帧的信息是没有必要的。
原文:https://blogs.msdn.microsoft.com/ericlippert/2005/08/08/recursion-part-four-continuation-passing-style/
递归——CPS(一)的更多相关文章
- 递归——CPS(二)
给出一个计算树深度的函数: function treeDepth(curtree) { if(curtree == null) return 0; else { var leftDepth = tre ...
- 递归——CPS(三)
JScript不是天然支持CPS,但是可以写一个分发引擎使得能工作在CPS风格下.一般只有一个活动的continuation,所以可以定义规则:JScript CPS 函数允许有返回,但是它们做的最后 ...
- 探索c#之递归APS和CPS
接上篇探索c#之尾递归编译器优化 累加器传递模式(APS) CPS函数 CPS变换 CPS尾递归 总结 累加器传递模式(Accumulator passing style) 尾递归优化在于使堆栈可以不 ...
- cps变换
网上看了很多内容,很少有给出一个准确的概念,它的英文全称是continuous passing style, 直译为连续传递样式,那么cps transform就是将一些原本不是continuous ...
- 如何设计一门语言(八)——异步编程和CPS变换
关于这个话题,其实在(六)里面已经讨论了一半了.学过Haskell的都知道,这个世界上很多东西都可以用monad和comonad来把一些复杂的代码给抽象成简单的.一看就懂的形式.他们的区别,就像用js ...
- haskell中的cps
cps全称叫continuation passing style,简要来讲就是告诉函数下一步做什么的递归方式,由于普通递归有栈溢出的问题,而cps都是尾递归(tail recursion),尾递归则是 ...
- 基于CPS变换的尾递归转换算法
前言 众所周知,递归函数容易爆栈,究其原因,便是函数调用前需要先将参数.运行状态压栈,而递归则会导致函数的多次无返回调用,参数.状态积压在栈上,最终耗尽栈空间. 一个解决的办法是从算法上解决,把递归算 ...
- 翻译连载 | 第 9 章:递归(下)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇
原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...
- 控制结构(11): Continuation passing style(CPS)
// 上一篇:控制结构(10)指令序列(opcode) [注释]: 这个笔记系列需要告一个段落了,收尾部分整理下几个时髦(The New Old Things)结构. 后面打算开一个算法方面的,重新学 ...
随机推荐
- 由ASP.NET所谓前台调用后台、后台调用前台想到HTTP
由ASP.NET所谓前台调用后台.后台调用前台想到HTTP 在由ASP.NET所谓前台调用后台.后台调用前台想到HTTP——理论篇中描述了一下ASP.NET新手的三个问题及相关的HTTP协议内容,在由 ...
- HTML meta viewport属性
什么是Viewport 手机浏览器是把页面放在一个虚拟的“窗口”(viewport)中,通常这个虚拟的“窗口”(viewport)比屏幕宽,这样就不用把每个网页挤到很小的窗口中(这样会破坏没有针对手机 ...
- Springboot 入门之Hello World
首先使用maven进行包加载和配置,但是你maven一定要配置好,maven的setting.xml文件一定要配置好,不然jar包加载不了的. <project xmlns="http ...
- 你的MongoDB Redis设置用户名密码了吗?看看shodan这款邪恶的搜索引擎吧!~
早上看新闻的时候看到了个醒目的新闻 开源中国:MongoDB 赎金事件持续发酵,究竟是谁之过? 博客园:MongoDB数据库勒索,中国受害者数量超乎你的想象,SOS! 1. 由于自己之前做过的项目,R ...
- C语言之函数的介绍
函数的介绍 遇到的问题: 1.代码看起来特别多,不简洁 2.修改起来非常麻烦,需要所有用到的地方都修改 函数就可以解决上述这两个问题 函数可以理解为一个打包带,就是把一段代码打包起来,用到的时候只要写 ...
- 一个问题:关于finally中return吞掉catch块中抛出的异常
今天遇到一个感觉很神奇的问题,记录一下问题以及自己分析问题的思路. 预警:不知道怎么看java字节码的朋友可能需要先看一下如何阅读java字节码才能看懂后面的解释. 我有一段程序: public cl ...
- C#中yield关键字理解
yield关键字之前用得较少,但是在做项目开发的过程中也遇到了,当时有点迷惑,就顺便研究学习了一下,以下是个人理解,不到之处欢迎拍砖!废话就到这,上代码: class Program { static ...
- Andy的生活
Andy是一个善良的男孩,希望优雅地生活. 现实是残酷的. 现实是,Andy像狗一样活着. Andy犯的错是,错误地认为狗一样的生活将在高考结束的那一刻结束. 大学四年时间根本没有静下心来学习,却在生 ...
- servlet笔记
开发servlet有三种方法: (1) 实现 Servlet接口 (2) 通过继承 GenericServlet (3) 通过继承 HttpServlet get提交和post提交的 ...
- NTSC色域(CIE1931)计算公式
色域(CIE1931)=ABS(RC[-6]*RC[-3]+RC[-4]*RC[-1]+RC[-2]*RC[-5]-RC[-6]*RC[-1]-RC[-4]*RC[-5]-RC[-2]*RC[-3]) ...