序言中,我们提到函数式编程的两大特征:无副作用、函数是第一公民。现在,我们先来深入第一个特征:无副作用。

无副作用是通过引用透明(Referential transparency)来定义的。如果一个表达式满足将它替换成它的值,而程序的行为不变,则称这个表达式是引用透明的。

现在,我们不妨进行一个尝试:我们来实现一些函数,但是这次有一个限制:只能用无副作用的表达式。

先以素数判定为例子,我们要写一个函数bool IsPrime(int n),它返回这个整数是不是素数。简单起见,我们采用最朴素的方法:依次检查2~n-1的整数,如果存在n的因子,则返回false,否则返回true.

这种问题的原始做法是使用循环,但是使用循环需要修改循环变量的值,从而产生副作用。

那怎么办了?有一个和循环关系紧密的概念——递归。递归不会改变变量的值,我们尝试用递归实现。

直接对IsPrime递归似乎不太可行,我们需要写一个辅助方法IsPrimeLoop。这个方法的参数除了n以外还有一个辅助参数acc,这个辅助参数起到类似循环变量的作用,它表示当前我们正在尝试的因子。

那这个函数要怎么实现呢?我们约定从小到大枚举整数,那么当acc == n时,循环就结束了,返回true。若acc != n,则循环继续。接着我们需要判断acc是不是n的因子,如果是,则n不是素数,返回false,否则继续递归循环。

借助这个辅助函数,我们只要调用IsPrimeLoop(n, 2)就可以判断了。代码如下:

private static bool IsPrimeLoop(int n, int acc) =>
(acc == n) || (n % acc != && IsPrimeLoop(n, acc + ));
public static bool IsPrime(int n) =>
n >= && IsPrimeLoop(n, );

注意到,这里的辅助函数IsPrimeLoop是私有的,因为这个函数是专门供IsPrime调用的,它的访问范围应该限制在IsPrime内。在C#6及以前,这是做不到的,只能把它设定为类私有尽可能减小访问范围。在C#7,我们可以利用内部函数进一步完善。

public static bool IsPrime(int n)
{
bool Loop(int acc) =>
(acc == n) || (n % acc != && Loop(acc + )); return n >= && Loop();
}

这时我们的Loop函数可以省略掉参数n,而且Loop的访问范围被限制在了IsPrime内。这样,我们就能在无副作用的前提下,实现素数的判定函数。

注意到,由于我们的IsPrime函数没有用到任何有副作用的表达式,所以,我们可以保证调用IsPrime也不会产生任何副作用。一般的,如果一个函数满足对它的调用一定是引用透明的,我们称这个函数为纯函数

下面我们来做一个练习,这里我需要你用递归实现阶乘函数int Fact(int n),当n>0时返回1*2*3*...*n的值,当n<=0时返回1,不考虑结果溢出的情况。你的实现不应该包含有副作用的表达式。

如果你完成了,请往下看。

下面我给出两个你可能的实现

public static int Fact(int n) =>
n <= ? : n * Fact(n - );
public static int Fact(int n)
{
int Loop(int acc, int result) =>
acc > n ? result : Loop(acc + , result * acc); return Loop(, );
}

当然,你的具体写法可能有所不同,但基本上可以归为两类。一类是像第一个那样,利用Fact(n)=n * Fact(n-1)进行递归;还有就是就像第二个那样,通过递归来让参数acc从1到n循环,并乘进一个结果变量result.

直观来看,第一个函数会更“递归”一点,而第二个函数则更像用递归实现的循环。为了进一步揭析这两个实现的区别,我们来手动展开一下两个版本的Fact(5)的递归过程:

版本一:

Fact(5) = 5 * Fact(4)

= 5 * 4 * Fact(3)

= 5 * 4 * 3 * Fact(2)

= 5 * 4 * 3 * 2 * Fact(1)

= 5 * 4 * 3 * 2 * 1 * Fact(0)

= 5 * 4 * 3 * 2 * 1 * 1

= 120

版本二:

Fact(5) = Loop(1, 1)

= Loop(2, 1)

= Loop(3, 2)

= Loop(4, 6)

= Loop(5, 24)

= Loop(6, 120)

= 120

发现没有?版本一的式子会逐渐变长,而版本二的式子长度则保持不变。这是因为,后者是尾递归。尾递归的定义为递归调用被立刻返回的递归。尾递归的特点是它理论上不需要额外的空间存储递归信息,就像我们展开式子那样,尾递归占用的空间是恒定的,而非尾递归调用则需额外的空间储存信息。事实上,尾递归和循环是等价的,因为尾递归可以想象成跳转到函数开头,只不过这个“跳转”是无副作用的。因此,我们可以用尾递归去实现循环,从而去除副作用。由于尾递归具有这种好处,我们通常尽可能的使用尾递归,只有在无法转换成尾递归,或者递归层数不大时,才使用非尾递归。

注意到我前面提到尾递归理论上不需要额外空间,但是很多语言在实现尾递归的时候会消耗栈空间的。比如JVM的尾递归会消耗栈空间,一些诸如Scala等编译到JVM的语言会将尾递归转换成循环从而防止栈溢出。但是C#编译器没有这个操作,那.NET在进行尾递归时会消耗栈空间吗?我们不妨来试一下。我的测试环境是.NET Core,使用之前定义的IsPrime函数,然后给它传入int.MaxValue,运行。

嗯,栈溢出了。

根据目前的实验结果,.NET在实现尾递归时会消耗栈空间。但是我用的是Debug模式,那切换到Release模式会怎样呢?

哈!没有溢出!

从上面实验可以看出,.NET Core在Debug模式下尾递归会消耗栈空间,Release模式不会。

因此,我们可以通过打开Release模式来避免尾递归产生栈溢出错误。

现在,递归相关的知识已经介绍完了。现在我们来讲讲递归的价值。

有的人觉得既然循环可以解决问题,那就没必要花时间去学什么递归;而有的人则觉得循环是魔鬼的,都应该改成递归。事实上,这两种极端的想法都是错误的。

递归的价值在于它能保证你写的函数是纯函数,从而降低一些意外的副作用产生的可能性。还记得序言的那个例子吗?那个程序就可以用尾递归实现来避免bug的产生。

当然,如果你要我写一个阶乘算法,或者写一个素数判断算法,我肯定用for循环。因为这个函数足够简单,我有自信做到,即使我的函数产生了副作用,但是这个副作用只是局部的,整个函数还是纯的函数。

但是,当程序复杂时,尤其是产生闭包时,这些副作用会比较隐晦,此时,使用尾递归能降低代码出错的几率。

尾递归还有一种好处:它能减少代码逻辑上的复杂性。我见过有一些好几重循环嵌套的程序,循环变量之间还相互依赖,逻辑非常复杂。但是,如果你把它改成尾递归,你就需要将循环转为一个或多个递归函数,从而使得逻辑结构更加的清晰。

最后,用一句话总结,递归应该减少你的负担,而不是成为你的负担

习题:

一、用尾递归改写序言中提到的副作用产生bug的例子。

二、对于斐波那契数列数列fib(n)定义为:当n<=2时,fib(n)=1;当n>2时,fib(n)=fib(n-1)+fib(n-2)。分别用尾递归和非尾递归实现fib,并比较两个实现的效率差异。你能解释其中的原因吗?

C#中的函数式编程:递归与纯函数(二)的更多相关文章

  1. C#中的函数式编程:递归与纯函数(二) 学习ASP.NET Core Razor 编程系列四——Asp.Net Core Razor列表模板页面

    C#中的函数式编程:递归与纯函数(二)   在序言中,我们提到函数式编程的两大特征:无副作用.函数是第一公民.现在,我们先来深入第一个特征:无副作用. 无副作用是通过引用透明(Referential ...

  2. 可爱的 Python : Python中的函数式编程,第三部分

    英文原文:Charming Python: Functional programming in Python, Part 3,翻译:开源中国 摘要:  作者David Mertz在其文章<可爱的 ...

  3. Java 中的函数式编程(Functional Programming):Lambda 初识

    Java 8 发布带来的一个主要特性就是对函数式编程的支持. 而 Lambda 表达式就是一个新的并且很重要的一个概念. 它提供了一个简单并且很简洁的编码方式. 首先从几个简单的 Lambda 表达式 ...

  4. C#中的函数式编程:序言(一)

    学了那么久的函数式编程语言,一直想写一些相关的文章.经过一段时间的考虑,我决定开这个坑. 至于为什么选择C#,在我看来,编程语言分三类:一类是难以进行函数式编程的语言,这类语言包括Java6.C语言等 ...

  5. (数据科学学习手札48)Scala中的函数式编程

    一.简介 Scala作为一门函数式编程与面向对象完美结合的语言,函数式编程部分也有其独到之处,本文就将针对Scala中关于函数式编程的一些常用基本内容进行介绍: 二.在Scala中定义函数 2.1 定 ...

  6. Apache Beam中的函数式编程理念

    不多说,直接上干货! Apache Beam中的函数式编程理念 Apache Beam的编程范式借鉴了函数式编程的概念,从工程和实现角度向命令式妥协. 编程的领域里有三大流派:函数式.命令式.逻辑式. ...

  7. C#中面向对象编程中的函数式编程详解

    介绍 使用函数式编程来丰富面向对象编程的想法是陈旧的.将函数编程功能添加到面向对象的语言中会带来面向对象编程设计的好处. 一些旧的和不太老的语言,具有函数式编程和面向对象的编程: 例如,Smallta ...

  8. Java中的函数式编程(二)函数式接口Functional Interface

    写在前面 前面说过,判断一门语言是否支持函数式编程,一个重要的判断标准就是:它是否将函数看做是"第一等公民(first-class citizens)".函数是"第一等公 ...

  9. 小白的Python之路 day3 函数式编程,高阶函数

    函数式编程介绍   函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计.函数就是面向过程的 ...

  10. Learning Python 012 函数式编程 1 高阶函数

    Python 函数式编程 1 高阶函数 高阶函数 Q:什么是高阶函数? A:一个函数接收另一个函数作为参数,这种函数就称之为高阶函数. 简单举个例子: def add(x, y, f): return ...

随机推荐

  1. mysql url 连接配置的一个小坑。 工作中不会遇到。 学习的时候会

    <property name="driverClassName"> <value>com.mysql.jdbc.Driver</value> & ...

  2. 什么是Docker??

    ​​Docker是一个轻量级虚拟机,也是一种Linux容器,它突破了以往的沙盒技术,解放了应用部署,让PaaS的应用场景更为广泛. ​ docker是通过内核虚拟化技术((namespaces及cgr ...

  3. 【Learning】带花树——一般图最大匹配

    一般图最大匹配--带花树 问题 ​ 给定一个图,求该图的最大匹配.即找到最多的边,使得每个点至多属于一条边. ​ 这个问题的退化版本就是二分图最大匹配. ​ 由于二分图中不存在奇环,偶环对最大匹配并无 ...

  4. Lintcode212 Space Replacement solution 题解

    [题目描述] Write a method to replace all spaces in a string with%20. The string is given in a characters ...

  5. 关于Videodownload helper的下载问题

    Videodownload helper是火狐浏览器的一个视频音频嗅探插件, 解压文件的时候处于解压状态还没有解压完的时候不能关闭原来的窗口 所以这个下载软件也同理.没有下载完一个视频之前不能把这个窗 ...

  6. android 获取wifi列表,如果你忽略了这个细节,可能你的软件会崩溃

    一:业务描述 最近公司有一个小需求,用户点击wifi扫描按钮(注意:是用户主动点击wifi扫描按钮),app去扫描附近的wifi,显示在listView中,仅此而已,app都不用去连接某个wifi,看 ...

  7. centos6上yum安装drbd(内核:2.6.32.696)

    author:headsen  chen date: 2017-11-20  15:11:21 notice: 个人原创,转载请注明,否则依法追究法律责任 前期准备: 两台机器:配置主机名分别为: l ...

  8. python学习03-数据类型

    一.基本数据类型--数字 布尔型 bool型只有两个值:True和False 之所以将bool值归类为数字,是因为我们也习惯用1表示True,0表示False. 以下是布尔值是False的各种情况: ...

  9. Greetings

    1,Common English Greetings Hey! Hey man! Hey girl! Hi. How's it going? How are you doing? What's up? ...

  10. SpringMVC的工作流程以及组件说明

    1. SpringMVC处理流程 2. SpringMVC架构 2.1 框架结构 2.2 框架流程 1. 用户发送请求至前端控制器DispatcherServlet. 2. DispatcherSer ...