C#中的递归APS和CPS模式详解

累加器传递模式(Accumulator passing style)

尾递归优化在于使堆栈可以不用保存上一次的返回地址/状态值,从而把递归函数当成一个普通的函数调用。

递归实际上是依赖上次的值,去求下次的值。 如果我们能把上次的值保存起来,在下次调用时传入,而不直接引用函数返回的值。 从而使堆栈释放,也就达到了尾递归优化的目的。

下面我们增加了一个acc的参数,它存储上次的值,在下次调用时传入。

代码如下:

static int Accumulate(int acc, int n)
    {
        if (n == 0)
            return acc;
        return accumulate(acc * n, n - 1);
    }

使用时Accumulate递归时,我们仅需要使用最后一次的返回值即可。 调用如下:

代码如下:

var ac = Accumulate(1, 20);

使用Lambda表达式实现尾递归阶乘:

代码如下:

static int AccumulateByLambda(int x)
    {
        Func<int, int, int> accumulate = null;
        accumulate = (acc, n) => n == 0 ? acc : Accumulate(acc * n, n - 1);
        return accumulate(1, x);
    }

CPS函数

CPS全称Continuation passing style,中文一般译为后继传递模式。

代码如下:

static int Times3(int x)
    {
        return x * 3;
    }
   Console.WriteLine(Times3(5));

上面函数将输入值乘以3,我们平常基本上都会这样写。 其实我们还可以用返回函数的C#语法,构造嵌套方式,把函数的调用变成调用链times3(3)(5)。

这种方式在数学上或函数式编程中是比较直观的,正常的,但在指令式语言c#中却不是那么直观。

CPS中的后继(Continuation)一词指的是计算的剩余部分,类似times3(3)(5)红色这部分。
例如:表达式a*(b+c)的运算过程有多个计算步骤。可以c#写成下面函数来表示:

代码如下:

Console.WriteLine(Mult(a,Add(b,c)))

操作步骤如下:

1.b与c相加。
2.将结果乘以a。
3.输出结果。

执行1步时,后续操作是2,3。执行2步时,后续操作是3。 使用CPS模式来改造下times3函数:

代码如下:

static void Times3CPS(int x, Action<int> continuation)
    {
        continuation(x * 3);
    }
Times3CPS(5, (reslut) => Console.WriteLine(result));

我们增加了一个表示后继操作3的函数参数,调用时传递后续操作,这就是CPS函数。

CPS变换

知道了CPS函数后,再详细看下CPS变换。

代码如下:

Console.WriteLine(Times3(5));
//CPS变换
Times3CPS(5, (reslut) => Console.WriteLine(result));

上面times3函数从直接调,到使用"后继传递操作"的过程就叫做CPS转换。
例如1:MAX函数的转换

代码如下:

static int Max(int n, int m)
{
    if (n > m)
        return n;
    else
        return m;
}
 Console.WriteLine(Max(3, 4));

我们把这max函数转换成CPS模式,需要下列步骤:
1:返回值修改成void
2:添加一个额外的类型参数 Action,T是原始返回类型。
3:使用后续操作表达式参数替代原来所有返回声明。

代码如下:

static void Max(int n, int m, Action<int> k)
{
    if (n > m)
        k(n);
    else
        k(m);
}
Max(3, 4, x => Console.WriteLine(x));

例如2:假如有3个函数Main、F、G,Main调用F、F调用G。

代码如下:

Console.WriteLine(F(1) + 1);
static int F(int n)
{
    return G(n + 1) + 1;
}
static int G(int n)
{
    return n + 1;
}

我们把F和G转换成CPS风格,和Max函数同样的转换步骤:

代码如下:

F(1, x => Console.WriteLine(x + 1));
static void F(int n, Action<int> k)
{
    G(n + 1, x => k(x + 1));
}
static void G(int n, Action<int> k)
{
    k(n + 1);
}

CPS尾递归

这是传统的递归阶乘:

代码如下:

static int Factorial(int n)
{
    if (n == 0)
        return 1;
    else
        return n * Factorial(n - 1);
}

使用同样的步骤,把递归转换成CPS尾递归:

代码如下:

Factorial(5, x => Console.WriteLine(x));
static void Factorial(int n, Action<int> continuation)
{
    if (n == 0)
        continuation(1);
    else
        Factorial(n - 1, x => continuation(n * x));
}

老赵-尾递归与Continuation

“计算n的阶乘,并将结果传入continuation方法并返回”,也就是“计算n - 1的阶乘,并将结果与n相乘,再调用continuation方法”。为了实现“并将结果与n相乘,再调用continuation方法”这个逻辑,代码又构造了一个匿名方法,再次传入Factorial方法。

总结

CPS模式是非常强大的,在很多方面都有使用,比如在编译器实现中CPS风格的解析器组合子、函数完成后回调。也可以说是把程序内部原本的控制操作,用CPS方法抽取出来暴露给程序员,例如文中的例子。

(0)

相关推荐

  • C#函数式编程中的递归调用之尾递归详解

    关于递归相信大家已经熟悉的不能再熟悉了,所以笔者在这里就不多费口舌,不懂的读者们可以在博客园中找到很多与之相关的博客.下面我们直接切入正题,开始介绍尾递归. 尾递归 普通递归和尾递归如果仅仅只是从代码的角度出发来看,我们可能发现不了他的特点,所以笔者利用两张堆栈上的图来展示具体的差距在哪,首先我们来看看普通的递归调用的情况,如下图1.1所示: 假设这里执行的函数是Func1,并且Func1中通过递归调用了自己,那么我们可以看到栈上在每次调用Func1的时候都会重新将函数返回地址等其他参数放入栈中

  • C#中尾递归的使用、优化及编译器优化

    递归运用 一个函数直接或间接的调用自身,这个函数即可叫做递归函数. 递归主要功能是把问题转换成较小规模的子问题,以子问题的解去逐渐逼近最终结果. 递归最重要的是边界条件,这个边界是整个递归的终止条件. 复制代码 代码如下: static int RecFact(int x) {     if (x == 0)         return 1;     return x * RecFact(x - 1); } RecFact(10); 上面是个经典阶乘函数的实现.这里分2步: 1.转换,把10的

  • c#斐波那契数列(Fibonacci)(递归,非递归)实现代码

    //Main 复制代码 代码如下: using System;using System.Collections.Generic;using System.Linq;using System.Text; namespace Fibonacci{    class Program    {        static void Main(string[] args)        {            Console.WriteLine("Would you like to know which

  • C#中的递归APS和CPS模式详解

    累加器传递模式(Accumulator passing style) 尾递归优化在于使堆栈可以不用保存上一次的返回地址/状态值,从而把递归函数当成一个普通的函数调用. 递归实际上是依赖上次的值,去求下次的值. 如果我们能把上次的值保存起来,在下次调用时传入,而不直接引用函数返回的值. 从而使堆栈释放,也就达到了尾递归优化的目的. 下面我们增加了一个acc的参数,它存储上次的值,在下次调用时传入. 复制代码 代码如下: static int Accumulate(int acc, int n)  

  • JavaScript中关于递归与回溯的实例详解

    目录 何为递归 构成递归条件 关于回溯 实际业务 组合问题 何为递归 递归作为一种算法在程序设计语言中广泛应用. 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量.递归的能力在于用有限的语句来定义对象的无限集合.需要注意的是,递归必须要用边界条件,否则很容易导致死循环 构成递归条件 子问题须与原始问题为同样的事,且更为简单

  • webpack项目中使用vite加速的兼容模式详解

    目录 前言 目的 要处理的问题 动手 共用 index.html 共用配置 兼容环境变量 自动导入 资源引入 svg-sprite-loader 替代方案 其他 效果 前言 随着公司前端工程越来越大,启动是无尽的等待,修改是焦急的等待. vite 到现在生态也起来了,就有了把项目改造成 vite 的想法,但是项目后面可能要依赖 qiankun 改造成微前端项目,现在 qiankun 对 vite 还没有好的解决方法,我就想采取一个折中的办法,保留 webpack,再兼容 vite,两条路都留着.

  • Android中Activity生命周期和启动模式详解

    Activity生命周期经典图解: 按键对生命周期的影响: BACK键: 当我们按BACK键时,我们这个应用程序将结束,这时候我们将先后调用onPause()->onStop()->onDestory()三个方法. 再次启动App时,会执行onCreate()->onStart()->onResume() HOME键: 当我们打开应用程序时,比如浏览器,我正在浏览NBA新闻,看到一半时,我突然想听歌,这时候我们会选择按HOME键,然后去打开音乐应用程序,而当我们按HOME的时候,A

  • Java中常用的设计模式之策略模式详解

    目录 优点 缺点 使用场景 一.实现方式 1.订单类型枚举类 2.订单处理接口 3.普通订单处理器 4.秒杀订单处理器 5.拼团订单处理器 6.下单管理器 二.测试 1.引入依赖 2.测试用例 总结 优点 1.算法可以自由切换. 2.避免使用多重条件判断. 3.扩展性良好. 缺点 1.策略类会增多. 2.所有策略类都需要对外暴露. 使用场景 1.如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为. 2.一个系统需要动态地在几种

  • Java中常用的设计模式之建造者模式详解

    目录 优点 缺点 使用场景 一.实现方式 二.实现方式 1.引入依赖 2.实现 三.测试 总结 优点 1.建造者独立,易扩展. 2.便于控制细节风险. 缺点 1.产品必须有共同点,范围有限制. 2.如内部变化复杂,会有很多的建造类. 使用场景 1.需要生成的对象具有复杂的内部结构. 2.需要生成的对象内部属性本身相互依赖. 一.实现方式 package com.asurplus.common.builder.style1; public class UserInfo { private Stri

  • Java中常用的设计模式之模板模式详解

    目录 优点 缺点 使用场景 一.实现方式 1.游戏抽象类 2.LOL游戏类 3.CF游戏类 二.测试 总结 优点 封装不变部分,扩展可变部分. 提取公共代码,便于维护. 行为由父类控制,子类实现. 缺点 每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大. 使用场景 1.有多个子类共有的方法,且逻辑相同. 2.重要的.复杂的方法,可以考虑作为模板方法. 一.实现方式 假设一个场景,我们在玩游戏的时候,都需要初始化加载游戏,然后开始游戏,最后结束游戏,这像是一套模板一样的操作

  • Java中常用的设计模式之工厂模式详解

    目录 优点 缺点 使用场景 一.实现方式 1.定义一个接口 2.定义两个接口实现类 3.定义一个工厂类 二.测试 总结 优点 1.一个调用者想创建一个对象,只要知道其名称就可以了. 2.扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以. 3.屏蔽产品的具体实现,调用者只关心产品的接口. 缺点 1.每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖.这并不是什么好事. 使用场景 1.日志记录器:记录

  • Java中的代理模式详解及实例代码

    java 代理模式详解 前言: 在某些情况下,一个客户不想或者不能直接引用一个对象,此时可以通过一个称之为"代理"的第三者来实现间接引用.代理对象可以在客户端和目标对象之间起到 中介的作用,并且可以通过代理对象去掉客户不能看到 的内容和服务或者添加客户需要的额外服务. 简单来说代理模式就是通过一个代理对象去访问一个实际对象,并且可以像装饰模式一样给对象添加一些功能. 静态代理 所谓静态代理即在程序运行前代理类就已经存在,也就是说我们编写代码的时候就已经把代理类的代码写好了,而动态代理则

  • JS中正则表达式只有3种匹配模式(没有单行模式)详解

    JS正则表达式对象模式仅有如下三种:  g (全文查找出现的所有 pattern) i (忽略大小写) m (多行查找) 即没有单行匹配模式,Singleline(单行模式):更改.的含义,使它与每一个字符匹配(包括换行符\n). 如java中 String regex = "(?s)(?<=interface).{0,500}(shutdown)";---------"."表示在一行. 但可以采用[\d\D]或[\w\W]或[\s\S]或(.|\s)*?来解

随机推荐