学了那么久的函数式编程语言,一直想写一些相关的文章。经过一段时间的考虑,我决定开这个坑。

至于为什么选择C#,在我看来,编程语言分三类:一类是难以进行函数式编程的语言,这类语言包括Java6、C语言等。这类语言由于不支持匿名函数等特性,进行函数式编程会比较困难;一类是自称“函数式编程语言”的语言,包括Scala、Clojure、F#、Haskell等。这类语言比较重视函数式编程,它的教学资料通常会包含函数式编程知识,因此这些语言的使用者大多也都已经掌握了函数式编程技巧;还有一类编程语言,它们不被称作函数式编程语言,却可以进行函数式编程。这些语言的使用者中懂得函数式编程的人相对较少,学习资料也较少提及函数式编程。这些语言包括Java8、C++11、C#、Rust、Kotlin、TypeScript、Python、Ruby等。

既然我的文章是要介绍函数式编程,首先我肯定不能选第一类,它们无法使用;而第二类编程语言的使用者已经掌握了函数式编程的技能。考虑到受众面,我的选择范围定在第三类语言内。最终我通过随机数选中了C#,如果我有精力我也会尝试一下其他语言。

说了这么多,那究竟什么是函数式编程呢?根据Scala之父Martin Odersky的说法,函数式编程有狭义和广义之分:狭义的函数式编程指的是表达式没有副作用的编程,满足这一特性的编程语言有Pure Lisp和不包含IO Monad与Unsafe operations的Haskell子集;而广义的函数式编程指的是函数是第一公民的语言,这个范围就大了很多,前面提到的第二类与第三类语言都属于广义的函数式编程。

而函数式编程的核心,就和这两个定义相关:没有副作用、函数是第一公民。

我们先来看副作用。我记得以前学C语言时有人喜欢用x++ + ++x为例去黑某个人写的臭名昭著的C语言的书。这个表达式实际上是一种未定义行为。但是,如果我们把它换成(x + 1) + (x + 2),这个语句就毫无歧义。问题在于x++、++x是有副作用的。如果一个表达式是无副作用的,我们就可以用这个表达式的值替换成它,而程序的行为不会发生改变。我们称这个性质为引用透明(Referential transparency)。就刚才的例子,假设x的值是3,那么对于(x + 1) + (x + 2)而言,我们可以把x + 1替换成它的值4,则表达式改写成4 + (x + 2),或者把x + 2替换成5而改写成(x + 1) + 5,这样的改写不会改变表达式的值。但是x++ + ++x就不可以,如果我们把x++换成3,那么表达式的值就会变。所以x++和++x不是引用透明的。

引用透明的一大特性是,我们可以改变引用透明的表达式的执行次序,而不用担心程序行为的变化。之所以x++ + ++x是未定义行为,是因为x++和++x不是引用透明的,从而导致x++和++x执行的先后顺序会影响整个表达式的值。而x + 1和x + 2的先后顺序则对表达式的值没有影响。这个特性在后面我们会用到。

下面再给一个例子,考虑这段C#代码

 class Program
{
static void Main()
{
for (int i = ; i < ; ++i)
{
System.Threading.Tasks.Task.Factory.StartNew(() =>
{
System.Threading.Thread.Sleep();
System.Console.WriteLine(i);
});
}
System.Console.ReadLine();
}
}

这段代码会输出什么?

你可能会以为它会以某种次序输出数字0到9,但实际输出是10个数字10.

为了能让程序输出数字0到9,我们需要这样修改程序:

 class Program
{
static void Main()
{
for (int i = ; i < ; ++i)
{
int _i = i;
System.Threading.Tasks.Task.Factory.StartNew(() =>
{
System.Threading.Thread.Sleep();
System.Console.WriteLine(_i);
});
}
System.Console.ReadLine();
}
}

如果你是JavaScript程序员,你可能会对这个策略有所熟悉。这是在循环中创建闭包(即使用了外部变量的匿名函数)时常遇到的坑。对于前一个程序,由于循环变量的i是变化的,因此i不满足引用透明,我们不能在创建闭包时就用i的值替换掉i,而由于Sleep语句存在,最终输出的时候i的值是10。而第二个程序输出的不是i,而是_i,_i满足一经初始化后不再被重新赋值,这是一个变量满足引用透明的重要特征。此时我们就可以用_i的值替换掉_i,从而程序能输出数字0~9.

从上面的例子可以看出,使用副作用可能会产生不经意的bug。因此,在函数式编程中,我们会尽量的少产生副作用。比如上面这段代码,最完美的方案是用我们后面会提到的尾递归。

函数式编程的另一个特点是函数是第一公民。在很多传统的编程语言中,函数有很多限制,比如我们不能在函数内部定义函数,我们不能创建一个函数类型的变量(注意:C语言的函数指针严格来讲不算。因为函数指针无法指向带闭包的函数)、我们不能将函数当成参数传给一个函数、不能创建一个没有名字的函数字面量等等。“函数是第一公民”的意思是,函数不应该受这些“歧视”。函数应该和其他类型拥有同等地位。当然,严格的满足函数是第一公民的语言也并不多。C#也是到了7才支持在函数内部创建函数。但对于函数式编程而言,函数至少要有的“权力”包括:创建没有名字的函数字面量(即匿名函数或Lambda表达式)、将函数作为参数传给其他参数。

我相信大家都用过Linq吧。Linq就是一个典型的把函数当第一公民的例子。在函数式编程中,我们将深挖函数作为第一公民的价值。

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. (数据科学学习手札48)Scala中的函数式编程

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

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

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

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

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

  7. C#中的函数式编程:递归与纯函数(二)

    在序言中,我们提到函数式编程的两大特征:无副作用.函数是第一公民.现在,我们先来深入第一个特征:无副作用. 无副作用是通过引用透明(Referential transparency)来定义的.如果一个 ...

  8. Java经典类库-Guava中的函数式编程讲解

    如果我要新建一个java的项目,那么有两个类库是必备的,一个是junit,另一个是Guava.选择junit,因为我喜欢TDD,喜欢自动化测试.而是用Guava,是因为我喜欢简洁的API.Guava提 ...

  9. C#中的函数式编程

    在函数式编程中,可以把函数看作数据.函数也可以作为参数,函数还可以返回函数.比如,LINQ就是基于函数式编程的. 两个例子引出函数式编程 语句式编程可能这样写: string result; ) { ...

随机推荐

  1. laypage 使用

    最近发现一个特别好用的前端分页插件,分享一下 <!doctype html> <html> <head> <meta charset="utf-8& ...

  2. centos ELK安装

    本文来自我的github pages博客http://galengao.github.io/ 即www.gaohuirong.cn ELK是进行日志收集分析用的,具体工作.原理.作用自行google. ...

  3. Android Camera 摄像 demo

          google 在Android 5.0推出 Camera2 这个类,用于替换 Camera,但是Camera2要求android sdk 最低版本为 minSdkVersion = 21 ...

  4. MySQL数据库基础(MySQL5.7安装、配置)

      写在前面: MySQL是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,目前属于 Oracle 旗下产品.MySQL 是最流行的关系型数据库管理系统之一,在 WEB 应用方面,MySQ ...

  5. Java 求集合的所有子集

    递归方法调用,求解集合的所有子集. package ch01; import java.util.HashSet; import java.util.Iterator; import java.uti ...

  6. eclipse快捷注释生成方法

    自动生成方法的注释格式,例如 /*** @param str* @return* @throws ParseException*/ 快捷键是alt+shift+j,将光标放在方法名上,按快捷键.会生成 ...

  7. vim操作备忘录

    vim操作备忘录 vim 备忘录 vim的书籍虽然看不不少,可是老是容易忘记,主要是自己操作总结过少,这个博客就主要用来记录一些比较常见的术语和操作,以防止自己再次忘记. <leader> ...

  8. 如何使用 OpenCV 打开摄像头获取图像数据?

    OpenCV 如何打开摄像头获取图像数据? 代码运行环境:Qt 5.9.1 msvc2015 32bit OpenCV 3.3.0 #include "include/opencv2/ope ...

  9. PAT1118. Birds in Forest (并查集)

    思路:并查集一套带走. AC代码 #include <stdio.h> #include <string.h> #include <algorithm> using ...

  10. scrapy 中日志的使用

    我在后台调试 在后台调试scrapy spider的时候,总是觉得后台命令窗口 打印的东西太多了不便于观察日志,因此需要一个日志文件记录信息,这样以后会 方便查找问题. 分两种方法吧. 1.简单粗暴. ...