从Scala循环里看函数式编程

我们用几分钟来想象一下自己是一名伐木工人,手里有林场里最好的斧子,因此你是工作效率最高的。突然有一天场里来了个推销的,他把一种新的砍树工具——链锯——给夸到了天上去。这人很有说服力,所以你也买了一把,不过你不懂得怎么用。你估摸着按照自己原来擅长的砍树方法,把链锯大力地挥向树干——不知道要先发动它。“链锯不过是时髦的样子货罢了”,没砍几下你就得出了这样的结论,于是把它丢到一边重新捡起用惯了的斧子。就在这个时候,有人在你面前把链锯给发动了……

学习一种全新的编程范式,困难并不在于掌握新的语言。毕竟能拿起这本书的读者,学过的编程语言少说也有一箩筐——语法不过是些小细节罢了。真正考验人的,是怎么学会用另一种方式去思考。

--《Functional Thinking》 Neal Ford

本文分为两个部分,第一部分是从大视角方面俯瞰函数式编程(Functional programming)的相关特点。第二部分则是结合具体语法,从Scala的循环中试着去体会函数式编程。理性和感性的认识一起来,让我们开始。

 

在讲函数式编程前,我们先回忆下现在大家最常用的面向对象编程(oop)。面向对象编程的核心思想是“将数据和对数据的操作进行紧密耦合”,在面向对象中,在对象里有我们的数据,有围绕数据的一系列操作方法。

这些对象对外隐藏这些具体的数据信息。它们通过接口,进行响应的方法或消息来和其它对象交互。因此,在面向对象编程中,抽象的核心是数据,这些数据通过接口等API对外展示。

 

  • 什么是函数式编程,有何特点

定义:简单说,"函数式编程"是一种"编程范式"(programming paradigm),也就是如何编写程序的方法论。

它属于"结构化编程"的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用。

函数式编程特点:

1. 函数是"第一等公民"

所谓"第一等公民"(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。

举例来说,下面代码中的print(_)就是一个函数,可以作为另一个函数的参数。

var arr = Array(1,2,3,4,5,6,7,78,78)

arr.foreach(print(_))

2. 只用"表达式",不用"语句"

"表达式"(expression)是一个单纯的运算过程,总是有返回值;"语句"(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。

我理解这里的原因,不仅仅是函数式编程的开发动机,一开始就是为了处理运算(computation),不考虑系统的读写(I/O)。而且还是需要对数据的连续操作,类似管道一样,如果是语句,没有返回值就无法为后续的表达式传入参数。这也是我理解的为什么Scala中函数中的最后一句表达式的返回值就是默认的整个函数的返回。

 

3. 没有"副作用"

所谓"副作用"(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。

函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。

 

4. 不修改状态

上一点已经提到,函数式编程只是返回新的值,不修改系统变量。因此,不修改变量,也是它的一个重要特点。大家可以回忆下在OOP中我们是怎么操作的,很多时候会把一个大对象传入一个方法中,然后方法会修改对象的值,最后再把相同的对象返回。

在其他类型的语言中,变量往往用来保存"状态"(state)。不修改变量,意味着状态不能保存在变量中。函数式编程使用参数保存状态,最好的例子就是递归。下面的代码是一个将字符串逆序排列的函数,它演示了不同的参数如何决定了运算所处的"状态"。

function reverse(string) {

if(string.length == 0) {

return string;

} else {

return reverse(string.substring(1, string.length)) + string.substring(0, 1);

}

}

由于使用了递归,函数式语言的运行速度比较慢,这是它长期不能在业界推广的主要原因。在Scala中有尾递归优化,在性能上会有一定的提升。

 

5. 引用透明

引用透明(Referential transparency),指的是函数的运行不依赖于外部变量或"状态",只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。这点对于后期的维护和测试很有帮助,想想以前一个大对象里的各种成员变量,还有外部变量(例如类变量)都有可能影响最后结果。现在只要传入的参数确定了,结果就能确定了,也就是等幂的。

有了前面的第三点和第四点,这点是很显然的。其他类型的语言,函数的返回值往往与系统状态有关,不同的状态之下,返回值是不一样的。这就叫"引用不透明",很不利于观察和理解程序的行为。

 

 

  • Scala中的循环

这部分将更多的用Scala的代码,特别是循环的代码来展现函数式编程的特点。采用同一个功能,两种方式实现。

功能要求:输入一个名字列表,去除列表中的单个字符元素,将其列表所剩的所有元素放在一个逗号分隔的字符串里,并且每个名字首字母要大写。

 

命令式方式:

  1. 遍历列表,判断长度大于1的元素
  2. 将步骤1选出的元素,首字母改大写,并在尾部加上逗号
  3. 将步骤2的结果存入可变长字符串
  4. 结束遍历,去除可变长字符串的最后一个逗号

 

Scala实现:

 

1、Filter函数:需要给filter方法一个判断条件或者返回true/false的函数,这个判断条件(函数)的输入类型要与集合元素类型一致,返回值是布尔型的。filter方法会对集合的每一个元素调用判断条件,当条件为true的时候则元素进入新的集合否则会被过滤掉。

2、Map函数: map会将每一条输入映射为一个新对象。{苹果,梨子}.map(去皮) = {去皮苹果,去皮梨子} 其中: “去皮”函数的类型为:A => B

3、Reduce函数:scala当中的reduce可以对集合当中的元素进行归约操作。默认为reduceLeft就是从左向右归约

 

不难发现,命令式的方式,更多的需要开发者关心到循环的细节,例如“下标”,需要根据“下标来处理列表”。但是函数式编程则更多的是关心传入几个函数(filter、map、reduce等)中的匿名函数的实现。真正的业务不是对“下标”的小心翼翼,而是匿名函数的实现。

就像在适应了java的GC后,很少有人再想自己去管理垃圾收集。同样,在函数式编程里迭代让位于高阶函数,出让控制权获得更好的体验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值