上一讲我们初步介绍了函数对象和 lambda 表达式,今天我们来讲讲它们的主要用途——函数式编程。
一个小例子
按惯例,我们还是从一个例子开始。想一下,如果给定一组文件名,要求数一下文件里的总文本行数,你会怎么做?
我们先规定一下函数的原型:
int count_lines(const char** begin,
const char** end);
也就是说,我们期待接受两个 C 字符串的迭代器,用来遍历所有的文件名;返回值代表文件中的总行数。
要测试行为是否正常,我们需要一个很小的 main 函数:
int main(int argc,
const char** argv)
{
int total_lines = count_lines(
argv + 1, argv + argc);
cout << "Total lines: "
<< total_lines << endl;
}
最传统的命令式编程大概会这样写代码:
int count_file(const char* name)
{
int count = 0;
ifstream ifs(name);
string line;
for (;;) {
getline(ifs, line);
if (!ifs) {
break;
}
++count;
}
return count;
}
int count_lines(const char** begin,
const char** end)
{
int count = 0;
for (; begin != end; ++begin) {
count += count_file(*begin);
}
return count;
}
我们马上可以做一个简单的“说明式”改造。用 istream_line_reader 可以简化 count_file 成:
int count_file(const char* name)
{
int count = 0;
ifstream ifs(name);
for (auto&& line :
istream_line_reader(ifs)) {
++count;
}
return count;
}
在这儿,要请你停一下,想一想如何进一步优化这个代码。然后再继续进行往下看。
如果我们使用之前已经出场过的两个函数,transform [1] 和 accumulate [2],代码可以进一步简化为:
int count_file(const char* name)
{
ifstream ifs(name);
istream_line_reader reader(ifs);
return distance(reader.begin(),
reader.end());
}
int count_lines(const char** begin,
const char** end)
{
vector<int> count(end - begin);
transform(begin, end,
count.begin(),
count_file);
return accumulate(
count.begin(), count.end(),
0);
}
这个就是一个非常函数式风格的结果了。上面这个处理方式恰恰就是 map-reduce。transform 对应 map,accumulate 对应 reduce。而检查有多少行文本,也成了代表文件头尾两个迭代器之间的“距离”(distance)。
函数式编程的特点
在我们的代码里不那么明显的一点是,函数式编程期望函数的行为像数学上的函数,而非一个计算机上的子程序。这样的函数一般被称为纯函数(pure function),要点在于:
会影响函数结果的只是函数的参数,没有对环境的依赖
返回的结果就是函数执行的唯一后果,不产生对环境的其他影响
这样的代码的最大好处是易于理解和易于推理,在很多情况下也会使代码更简单。在我们上面的代码里,count_file 和 accumulate 基本上可以看做是纯函数(虽然前者实际上有着对文件系统的依赖),但 transform 不行,因为它改变了某个参数,而不是返回一个结果。下一讲我们会看到,这会影响代码的组合性。
我们的代码中也体现了其他一些函数式编程的特点:
函数就像普通的对象一样被传递、使用和返回。
代码为说明式而非命令式。在熟悉函数式编程的基本范式后,你会发现说明式代码的可读性通常比命令式要高,代码还短。
一般不鼓励(甚至完全不使用)可变量。上面代码里只有 count 的内容在执行过程中被修改了,而且这种修改实际是 t