C#中的函数式编程:序言(一)
学了那么久的函数式编程语言,一直想写一些相关的文章。经过一段时间的考虑,我决定开这个坑。
至于为什么选择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#中的函数式编程:序言(一)的更多相关文章
- C#中的函数式编程:递归与纯函数(二) 学习ASP.NET Core Razor 编程系列四——Asp.Net Core Razor列表模板页面
C#中的函数式编程:递归与纯函数(二) 在序言中,我们提到函数式编程的两大特征:无副作用.函数是第一公民.现在,我们先来深入第一个特征:无副作用. 无副作用是通过引用透明(Referential ...
- 可爱的 Python : Python中的函数式编程,第三部分
英文原文:Charming Python: Functional programming in Python, Part 3,翻译:开源中国 摘要: 作者David Mertz在其文章<可爱的 ...
- Java 中的函数式编程(Functional Programming):Lambda 初识
Java 8 发布带来的一个主要特性就是对函数式编程的支持. 而 Lambda 表达式就是一个新的并且很重要的一个概念. 它提供了一个简单并且很简洁的编码方式. 首先从几个简单的 Lambda 表达式 ...
- (数据科学学习手札48)Scala中的函数式编程
一.简介 Scala作为一门函数式编程与面向对象完美结合的语言,函数式编程部分也有其独到之处,本文就将针对Scala中关于函数式编程的一些常用基本内容进行介绍: 二.在Scala中定义函数 2.1 定 ...
- Apache Beam中的函数式编程理念
不多说,直接上干货! Apache Beam中的函数式编程理念 Apache Beam的编程范式借鉴了函数式编程的概念,从工程和实现角度向命令式妥协. 编程的领域里有三大流派:函数式.命令式.逻辑式. ...
- C#中面向对象编程中的函数式编程详解
介绍 使用函数式编程来丰富面向对象编程的想法是陈旧的.将函数编程功能添加到面向对象的语言中会带来面向对象编程设计的好处. 一些旧的和不太老的语言,具有函数式编程和面向对象的编程: 例如,Smallta ...
- C#中的函数式编程:递归与纯函数(二)
在序言中,我们提到函数式编程的两大特征:无副作用.函数是第一公民.现在,我们先来深入第一个特征:无副作用. 无副作用是通过引用透明(Referential transparency)来定义的.如果一个 ...
- Java经典类库-Guava中的函数式编程讲解
如果我要新建一个java的项目,那么有两个类库是必备的,一个是junit,另一个是Guava.选择junit,因为我喜欢TDD,喜欢自动化测试.而是用Guava,是因为我喜欢简洁的API.Guava提 ...
- C#中的函数式编程
在函数式编程中,可以把函数看作数据.函数也可以作为参数,函数还可以返回函数.比如,LINQ就是基于函数式编程的. 两个例子引出函数式编程 语句式编程可能这样写: string result; ) { ...
随机推荐
- CENTOS6.6下mysql MMM架构搭建
本文来自我的github pages博客http://galengao.github.io/ 即www.gaohuirong.cn MMM(Master-Master replication mana ...
- 通过云主机(网关机)远程登录内网mysql
国内的一些云主机平台(UCloud,阿里云,腾讯云等)走的都是网关机+内网机(即局域网)模式,网关机代理外网访问,不能直接连接内网机器.本文介绍通过远程登录云主机,并设置本地代理的方式,通过sqlyo ...
- python调用百度语音(语音识别-斗地主语音记牌器)
一.概述 本篇简要介绍百度语音语音识别的基本使用(其实是斗地主时想弄个记牌器又没money,抓包什么的又不会,只好搞语音识别的了) 二.创建应用 打开百度语音官网,产品与使用->语音识别-> ...
- Ubuntu忘记root密码怎么办?
http://www.linuxidc.com/Linux/2016-05/131256.htm
- 如何使用 window api 转换字符集?
//宽字符转多字节 std::string W2A(const std::wstring& utf8) { int buffSize = WideCharToMultiByte(CP_ACP, ...
- UVA1601 状态搜索
很有意思的一道题,就是迷宫问题的增强版.但是细节很多,有一个技巧就是把每个可以走的位置编号方便状态判重. AC代码: #include<cstdio> #include<cstrin ...
- 算法提高 拿糖果 线性DP
题目链接:拿糖果 思路:首先给小于根号n的素数打表.d(i)表示当前剩余i颗糖,最多可以拿到多少糖. 转移方程:d(i) = max(d(i), k + d(i - 2 * k)),此处k表示 ...
- http协议——cookie详解
http是无状态的,所以引入了cookie来管理服务器与客户端之间的状态 与cookie相关的http首部字段有: 1.Set-Cookie:它一个响应首部字段,从服务器发送到客户端,当服务器想开始通 ...
- swap分区的扩展
Linux中Swap(即:交换分区),类似于Windows的虚拟内存,就是当内存不足的时候,把一部分硬盘空间虚拟成内存使用,从而解决内存容量不足的情况.swap分区在非高内存的服务器上必不可少,但是s ...
- 2015最新iOS学习线路图
iOS是由苹果公司开发的移动操作系统,以xcode为主要开发工具,具有简单易用的界面.令人惊叹的功能,以及超强的稳定性,已经成为iPhone.iPad 和iPod touch 的强大基础:iOS 内置 ...