程序中为什么需要栈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。

总结过程如下:

  1. 程序传给foo的参数为 1 和 terminate
  2. foo传给bar的参数为 foocontinuation
  3. bar传给 foocontinuation 的参数为 10
  4. foocontinuation 给出 blah 的参数为 1,10 和 terminate
  5. blah 将参数相加得到11,然后将 11 和 terminate 传给 print
  6. print 打印 11,然后调用terminate,结束程序

实际中,JScript不知道这些函数都不返回值。JScript也不能聪明地意识到即使这些函数返回了值,这些函数在子程序调用完成后,也不再做任何事情,因此保持堆栈中旧帧的信息是没有必要的。

原文:https://blogs.msdn.microsoft.com/ericlippert/2005/08/08/recursion-part-four-continuation-passing-style/

递归——CPS(一)的更多相关文章

  1. 递归——CPS(二)

    给出一个计算树深度的函数: function treeDepth(curtree) { if(curtree == null) return 0; else { var leftDepth = tre ...

  2. 递归——CPS(三)

    JScript不是天然支持CPS,但是可以写一个分发引擎使得能工作在CPS风格下.一般只有一个活动的continuation,所以可以定义规则:JScript CPS 函数允许有返回,但是它们做的最后 ...

  3. 探索c#之递归APS和CPS

    接上篇探索c#之尾递归编译器优化 累加器传递模式(APS) CPS函数 CPS变换 CPS尾递归 总结 累加器传递模式(Accumulator passing style) 尾递归优化在于使堆栈可以不 ...

  4. cps变换

    网上看了很多内容,很少有给出一个准确的概念,它的英文全称是continuous passing style, 直译为连续传递样式,那么cps transform就是将一些原本不是continuous ...

  5. 如何设计一门语言(八)——异步编程和CPS变换

    关于这个话题,其实在(六)里面已经讨论了一半了.学过Haskell的都知道,这个世界上很多东西都可以用monad和comonad来把一些复杂的代码给抽象成简单的.一看就懂的形式.他们的区别,就像用js ...

  6. haskell中的cps

    cps全称叫continuation passing style,简要来讲就是告诉函数下一步做什么的递归方式,由于普通递归有栈溢出的问题,而cps都是尾递归(tail recursion),尾递归则是 ...

  7. 基于CPS变换的尾递归转换算法

    前言 众所周知,递归函数容易爆栈,究其原因,便是函数调用前需要先将参数.运行状态压栈,而递归则会导致函数的多次无返回调用,参数.状态积压在栈上,最终耗尽栈空间. 一个解决的办法是从算法上解决,把递归算 ...

  8. 翻译连载 | 第 9 章:递归(下)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇

    原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...

  9. 控制结构(11): Continuation passing style(CPS)

    // 上一篇:控制结构(10)指令序列(opcode) [注释]: 这个笔记系列需要告一个段落了,收尾部分整理下几个时髦(The New Old Things)结构. 后面打算开一个算法方面的,重新学 ...

随机推荐

  1. npm ERR! Error: socket hang up

    when i use npm to install express, it goes this message: npm info it worked if it ends with ok npm i ...

  2. C#中鼠标划过按钮时候的提示信息

    ToolTip p = new ToolTip();            p.ShowAlways = true;            p.SetToolTip(this.Buton1, &quo ...

  3. Object-c学习之路(oc点语法)

    最近想学习object-c了自己上网找了一些资料自学了一下:oc中的点语法是为了java.c等的程序员更好的上手而添加的功能. 主函数 // // main.m // OcTest1 // // Cr ...

  4. Windows7和Archlinux双系统硬盘安装笔记

    俗话说,好记性不如烂笔头,这些东西也都是我Google来的,做个笔记以后自己安装也方便些. 因为官方wiki的Beginners' Guide讲的非常好,大部分步骤按照wiki一步一步来就好了,这里只 ...

  5. 四.redis 事务

    redis对事务的支持目前还比较简单.redis只能保证一个client发起的事务中的命令可以连续的执行,而中间不会插入其他client的命令. 由于redis是单线程来处理所有client的请求的所 ...

  6. 如何安装ArchLinux

    如何安装ArchLinux   本文基于ArchLinux(https://www.archlinux.org/)Current Release: 2013.08.01的ISO写的安装教程! ISO下 ...

  7. Go Revel 学习指南

    Go Revel 学习指南 CONTROLLERS(控制器) Routing(路由)http://www.cnblogs.com/hangxin1940/p/3267065.html Paramete ...

  8. Oracle用脚本语言导入SCOTT用户

    许多Oracle新手都遇到这样的问题,安装Oracle之后没有SCOTT用户,那就自己加入吧,打开Oracle 命令窗口复制下面SQL脚本直接输入就行了,包含了测试学习的DEPT.EMP.BONUS. ...

  9. zTree应用实例详讲

    zTree应用实例详讲(1) 因为项目的需要,要创建一棵动态的文件树,此树除了实现异步获取子节点外,还要实现对树节点的增.删.改.查.移动.重命名.批量删除.批量移动. 每一个操作都要和数据库打交道. ...

  10. Java网络请求getInputStream异常

    今天调试网络请求部分时,当getInputStream失败时直接抛出异常.解决方法时在getInputStream之前获取ResponseCode if( connection.getResponse ...