面向对象编程通过封装不确定因素来使代码能被人理解;函数式编程通过尽量减少不确定因素来使代码能被人理解。
Java接管内存分配减轻了我们的负担,函数式编程语言让我们用高阶抽象从容取代基本的控制结构,也有着同样的意义。“将琐碎的细节交托给运行时,令繁冗的实现化作轻巧”
交织”(complect):穿插缠绕地合为一体,使错综复杂。命令式编程风格常常迫使我们出于性能考虑,把不同的任务交织起来,以便能够用一次循环来完成多个任务。而函数式编程用map()、filter()这些高阶函数把我们解放出来,让我们站在更高的抽象层次上去考虑问题,把问题看得更清楚。
封装、作用域、可见性等面向对象编程(OOP)构造,这些机制的存在意义,都是为了精细地控制谁能够感知状态和改变状态。而当涉及多线程的时候,对状态的控制就更复杂了。
大多数函数式语言在这个问题上采取了另一种做法,它们认为,与其建立种种机制来控制可变的状态,不如尽可能消灭可变的状态这个不确定因素。其立论的根据是这样的:假如语言不对外暴露那么多有出错可能的特性,那么开发者就不那么容易犯错。
OOP的世界提倡开发者针对具体问题建立专门的数据结构,相关的专门操作以“方法”的形式附加在数据结构上。函数式编程语言实现重用的思路很不一样。函数式语言提倡在有限的几种关键数据结构(如list、set、map)上运用针对这些数据结构高度优化过的操作,以此构成基本的运转机构。开发者再根据具体用途,插入自己的数据结构和高阶函数去调整机构的运转方式。
比起一味创建新的类结构体系,把封装的单元降低到函数级别,更有利于达到细粒度的、基础层面的重用。
当垃圾收集成为主流,一下子将若干难以调试的错误类别连根拔起,程序员也因为运行时接管了复杂且容易出错的内存管理而获得解脱。函数式编程希望在算法编写上给予程序员同样的帮助,一方面程序员得以在更高的抽象层次上工作,另一方面运行时也有了执行复杂优化的自由空间。开发者从中获得的好处体现在更低的复杂性和更高的性能,这点与垃圾收集相同
命令式编程是按照“程序是一系列改变状态的命令”来建模的一种编程风格。传统的for循环是命令式风格的绝好例子:先确立初始状态,然后每次迭代都执行循环体中的一系列命令。
命令式编程鼓励程序员将操作安排在循环内部去执行。
package com.nealford.functionalthinking.trans;
import java.util.List;
public class TheCompanyProcess {
public String cleanNames(List<String> listOfNames) {
StringBuilder result = new StringBuilder();
for(int i = 0; i < listOfNames.size(); i++) {
if (listOfNames.get(i).length() > 1) {
result.append(capitalizeString(listOfNames.get(i))).append(",");
}
}
return result.substring(0, result.length() - 1).toString();
}
public String capitalizeString(String s) {
return s.substring(0, 1).toUpperCase() + s.substring(1, s.length());
}
}
// java8 函数式实现
public String cleanNames(List<String> names) {
if (names == null) return "";
return names
.stream()
.filter(name -> name.length() > 1)
.map(name -> capitalize(name))
.collect(Collectors.joining(","));
}
private String capitalize(String e) {
return e.substring(0, 1).toUpperCase() + e.substring(1, e.length());
}
函数式编程将程序描述为表达式和变换,以数学方程的形式建立模型,并且尽量避免可变的状态。
listOfEmps
-> filter(x.length > 1)
-> transform(x.capitalize)
-> convert(x + "," + y)
函数式语言可以帮助我们轻松搭建出上面的概念性解答模型,同时又不必操心各种实现细节。
向函数式思维靠拢,意味着我们逐渐学会何时何地应该求助于这些更高层次的抽象,不要再一头扎到实现细节里去。
函数式编程的主旨就是在使用函数的组合来实现运算。 这里,主体是各种函数之间的组合,以完成某种计算为目的,而输入的数据是不重要的。
组合函数,也是实现函数式编程 “无状态” 的过程。因为可以直接把一个函数的返回值作为参数传给下一个函数,避免中间状态。
学会用更高层次的抽象来思考有什么好处?
首先,会促使我们换一种角度去归类问题,看到问题的共性。
其次,让运行时有更大的余地去做智能的优化。有时候,在不改变最终输出的前提下,调整一下作业的先后次序会更有效率(例如减少了需要处理的条目)。
第三,让埋头于实现细节的开发者看到原本视野之外的一些解决方案。
例如上面的Java实现要改成多线程的话,需要的工作量可不小。由于我们自己控制着低层次的迭代细节,那么线程相关的代码也就只好由我们自己动手穿插进去。可是换作Scala的实现,我们只要在stream上多调用一次par方法就可以了。
val parallelResult = employees
.par
.filter(_.length() > 1)
.map(_.capitalize)
.reduce(_ + "," + _)
但Java 8(引入函数式编程的一些特性)实现要达到相同的并行化效果,也只需要做几乎一样的简单改动。
public String cleanNamesP(List<String> names) {
if (names == null) return "";
return names
.parallelStream()
.filter(n -> n.length() > 1)
.map(e -> capitalize(e))
.collect(Collectors.joining(","));
}
总结
我们在更高的抽象层次上做事情,运行时才好去优化低层次的细节。
多从结果着眼,少纠结具体的步骤。
不要再让那些迭代、变换、化约如何进行的低层次细节占据你的思维,多想想哪些问题其实可以归结为这几样基本操作的排列组合吧。
函数组合实现多个函数合并,最小功能的复用,多个小功能合成一个大功能,多个大功能再一起完成任务。
整洁架构中:实例可以用函数代替,用例就是多个函数组合来办一件事的函数
// 伪代码,不考虑细节,只考虑要做的事
// 上传文件->获取文件md5->检查是否上传等信息->开始上传->报告进度
uploadFiles(files).getFileMd5().checkUploadInfo().start().onProgressChange()