OO makes code understandable by encapsulating moving parting, but FP makes code understandable by minimizing moving parts. -Michael Feathers
什么是函数式编程(FP,Functional Programming),目前似乎并没有一个让大家普遍认可的定义,今天在这里也不打算讨论这个问题。本文的目的就是带大家感受一下函数式编程的感觉,并不是一个函数式编程的教程。
伊始
我们打算从一个简单例子带大家走进函数式编程的世界,我打算从三种不同的语言来介绍,如果读者不熟悉某门语言的话,或者只想阅读自己熟悉的语言,完全可以跳过不感兴趣的语言,我会尽量把每一种语言当成一个独立的单元来讲。
破冰
因为只是带大家体验一下,我们选取的题目相对来说比较简单。
- 问题
输入一系列文件,需要我们统计每个文件的行数。
- 说明
为了简化问题,这里我们只去统计每个文件中换行符\n
的个数,假定最后一行也有一个换行符(当然,你也可以直接加上1
得到最后的真实行数)。
- 命令式
如果以传统命令式的编程方式,大概需要进行以下的分析0
. 定义一个文件行数统计列表,用来保存每个文件的行数;1
. 循环打开文件;2
. 定义一个计数器变量count
;3
. 读取文件字符,判断是否为回车符\n
,是则将count
加1
;4
. 当前文件读取结束,存储当前文件的行数;5
. 循环结束,返回统计结果。
上面的思维流程非常清晰明了,也非常符合我们平常的开发流程,下面我们进入到各个语言中去,看看从函数式的角度去看看如何解决。
C++
- 表达
本例中我们用vector
来表示输入的文件集合,用vector
来表示对应的文件行数。
- 命令式
这里我们先以命令式的方式实现这个需求,以便大家更加直观的感受其与函数式思维的区别
std::vector count_lines_in_files(const std::vector& files) {
std::vector lines;
char c = 0;
for (const auto& file : files) {
int count = 0;
std::ifstream in{file};
while (in.get(c)) {
if (c == '\n') count++;
}
lines.push_back(count);
}
return lines;
}
- 函数式
我们定义一个辅助函数open_file
来转换文件到文件流。
std::ifstream open_file(std::string file) {
return std::ifstream{file};
}
然后统计文件流当中的换行符\n
个数
int count_lines(std::ifstream in) {
return std::count(std::istreambuf_iterator(in),
std::istreambuf_iterator(),
'\n');
}
最后我们分别将上面的转换函数open_file
和统计函数count_lines
分别映射到输入的文件的列表,在C++
语言中,映射的操作就是std::transform
,在python
和Java
中就是map
函数,这类函数在函数式编程中被称为高阶函数,他们可以接受函数作为形参,将函数视为一等公民。
std::vector count_lines_in_files(const std::vector& files) {
std::vector lines(files.size());
std::vector filestreams(files.size());
std::transform(std::begin(files), std::end(files),
std::begin(filestreams), open_file);
std::transform(std::begin(filestreams), std::end(filestreams),
std::begin(lines), count_lines);
return lines;
}
上面的这种解决方法,我们没有去关心如何打开文件,以及统计是如何进行的。
上述的程式我们只是告诉计算机我们希望在给定的流中去统计换行符\n
,这里我们将统计换行符这个动作count_lines
封装起来,就是想说明我们不关心count_lines
这个动作,这次是统计换行符\n
,下次可以是统计任意的字符或者单词,everything
,只要符合我们的接口契约就可以。
函数式编程的主要思想 —— 使用抽象去表明我们的目的,而不是说明如何去做,只需指明输入转换为期望的输出。
上面的程序,我们可以range
和range transformations
来实现(这个是C++20
才引入的),函数的意图将会更加清晰明了。
std::vector count_lines_in_files(const std::vector& files) {
return files | transform(open_file) | transform(count_lines);
}
这里range
使用管道|
操作符表示通过转换来传递一个集合,有兴趣的读者可以去研究一下。
Java
- 表达
本例中我们可以用List
来表示输入的文件集合,然后构造一个List
来表示对应的文件行数。
- 命令式
关于命令式的解答这里就不在演示了,有兴趣的读者可以自己尝试。
- 函数式
下面我们看看java
如何通过函数式思维解决这个问题,我们先定义一个统计函数countLine
用来统计文件中换行符\n
的个数。
static long countLine(File file) {
long count = 0;
try (Stream lines = Files.lines(Paths.get(file.toURI()), Charset.defaultCharset())) {
count = lines.map(line -> Arrays.stream(line.split("\n"))).count();
} catch (IOException e) {
}
return count;
}
有了这个辅助函数,我们利用Java8
的stream
特性结合映射操作map
,将输入的文件列表映射到统计函数countLine
,最后使用收集器执行终端操作。
public static List countFilesLine(List files) {
return files.stream()
.map(LinesCounter::countLine)
.collect(toList());
}
上述LinesCounter::countLine
写法被称为方法引用,如果对stream
或者方法引用不熟悉的读者可以参考Richard Warburton
所著的java 8 函数式编程
一书。
Python
- 表达
由于python
语言自身的特性,不想C++
和Java
对类型要求严格,本例中输入文件列表和输出文件行数我们都可以使用python
内置的list
数据类型来表达。
- 函数式
这个例子对于python语言比较简单,我们故意使用下面的方式去解答这个问题,真正的时候我们可能不会去这么做。
def open_file(file):
with open(file, 'r') as f:
return f.read()
def count_line():
return lambda file: open_file(file).count('\n')
def count_lines_in_files(files):
return list(map(count_line(), files))
当然我们可以利用了python
的readlines
返回文件所有行的列表,然后通过映射操作计算列表长度即可。
def read_lines(file):
with open(file) as fin: return fin.readlines()
def count_lines_in_files(files):
return list(map(len, map(read_lines, files)))
不过,上面的方法有个弊端,由于readlines()方法读取整个文件所有行,保存为列表,当文件过大的时候会占据过大内存。
- 备注
虽然这里我介绍了一些FP
的优点,但并不表示命令式就一无是处,相反,本人是OO
的坚实拥护者,同时也是FP
的粉丝,个人更加倾向于多范式的结合编程。
意犹未尽
看完这个例子,有没有激起你对函数式编程的兴趣,若有,抓紧行动起来吧…