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

至于为什么选择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. 自己制作ssl证书:自己签发免费ssl证书,为nginx生成自签名ssl证书

    这里说下Linux 系统怎么通过openssl命令生成 证书. 首先执行如下命令生成一个key openssl genrsa -des3 -out ssl.key 1024 然后他会要求你输入这个ke ...

  2. 洛谷 P1486 [NOI2004]郁闷的出纳员【Treap】题解+AC代码

    题目描述 OIER公司是一家大型专业化软件公司,有着数以万计的员工.作为一名出纳员,我的任务之一便是统计每位员工的工资.这本来是一份不错的工作,但是令人郁闷的是,我们的老板反复无常,经常调整员工的工资 ...

  3. [JSOI2008]最大数maxnumber

    [JSOI2008]最大数maxnumber 标签: 线段树 单独队列 题目链接 题解 线段树裸题. 如果一直RE可能是你用的cin/cout. Code #include<cstdio> ...

  4. springboot入门_模板

    springboot中已经不推荐使用jsp,而是推荐使用模板,如freemarker,thymeleaf等,本文记录在sprigboot中使用模板. 创建一个maven的springboot工程, f ...

  5. Java反射获取字节码以及判断类型

    一.获取类的字节码的三种方法: 1.使用Class.class   Class<?> c1=String.class; 2.使用实例.getClass()   String s= Clas ...

  6. Delphi 添加外部Form单元的方法!

    我用到的环境是 RAD Studio 10.2.2 有时候,需要把某个Form单元  添加到其他的工程!  此时,如果直接添加或者拖拉 .pas单元到目标工程,是无法把.pas包含的Form添加进去的 ...

  7. java7 - JDK

    一.学习大纲: 1. 熟练使用 JDK 文档 2. 软件包 java.lang 提供利用 Java 编程语言进行程序设计的基础类. 3. 软件包 java.math 提供用于执行任意精度整数算法 (B ...

  8. 转:【web前端开发】浏览器兼容性处理大全

    解决思路: ①.写代码的时候遵循W3C标准,按照最新稳定版本的IE或WebKit内核浏览器进行编码 ②.遇到部分无法全面解决浏览器兼容的时候,采取CSS的hack手段进行针对性微调.简单的说,CSS ...

  9. PAT甲级 1004 树

    思路:直接遍历整棵树判定每个结点是否有孩子,没有则把当前高度的叶子节点数加一. AC代码 #include <stdio.h> #include <string.h> #inc ...

  10. openstack-ocata-身份验证2

    Identity service 一.身份服务概述 OpenStack身份管理服务提供一个单点集成身份验证.授权和目录服务. 身份服务通常是第一个服务用户与之交互.一旦身份验证,最终用户可以使用自己的 ...