程序中为什么需要栈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. web开发相关

    Session相关 关了浏览器session当然仍然存在,因为session是储存在服务器端的,而服务器是不可能知道你有没有关掉浏览器. 服务器只是简单的保持session接受用户请求,只有当sess ...

  2. 字符串拼接 strcat ;数组和指针的区别

    问题:字符串拼接 strcat 方法1: 开辟新空间,存放结果: #include <stdio.h> #include <stdlib.h> #include <str ...

  3. WPF中StackPanel的使用方法

    StackPanel 1.StackPanel:释义为是最简单的控制面板,它把其中的UI元素按横向或纵向堆积排列. 2.常用属性:width:获取或设置元素的宽度.Orientation:用于控制面板 ...

  4. socket1

    socket的那些事(1) TCP/IP 简单介绍 应用层 (Application):应用层是个很广泛的概念,有一些基本相同的系统级 TCP/IP 应用以及应用协议,也有许多的企业商业应用和互联网应 ...

  5. 框架的设计之IRepository还是IRepository<T>

    [Yom框架]漫谈个人框架的设计之[是IRepository还是IRepository<T>]? 前言                                            ...

  6. HTML5学习+javascript学习:打飞机游戏简介以及Model层

    本着好记性不如烂博客以及分享成功的喜悦和分享失败的苦楚,今天我来分享下一个练手项目:打飞机游戏~从小就自己想做游戏,可是一直没有机会.HTML5给了我们这个平台,这个平台可以有很多以前想都不敢想的东西 ...

  7. Linux中添加管理员权限问题:xxx is not in the sudoers file. This incident will be reported.

    在各个不同版本的linux中添加拥有管理员权限账户有不同的简便方式. 问题: 今天遇见将新添用户添加到root用户组后,运行sudo仍然提示 ”xxx is not in the sudoers fi ...

  8. LINUX下编译安装最新版本mysql

    通过参考其他文章 1.下载安装mysql-5.5.30.tar.gz与cmake.2.8.11.2.tar.gz (1)先安装cmake(mysql5.5以后是通过cmake来编译的) [root@ ...

  9. React的生命周期

    我们先来看一张图,其实看完这张图基本就懂了,如果还不懂,请继续往下看. getDefaultProps 执行过一次后,被创建的类会有缓存,映射的值会存在this.props,前提是这个prop不是父组 ...

  10. java Integer 源码学习

    转载自http://www.hollischuang.com/archives/1058 Integer 类在对象中包装了一个基本类型 int 的值.Integer 类型的对象包含一个 int 类型的 ...