相信大多数程序员接触到的第一门编程语言都是 C 语言,我们的编程思维基本上都是 C 语言帮助我们塑造的,这种编程思想会贯彻我们整个职业生涯。C 语言是一门面向过程的语言,表达力强、非常直观,是初学编程接受程度较高的语言。但是随着程序应用越来越广泛,面向过程的编程思想对现实世界的抽象稍显不足。

一般接下来接触的就是面向对象的编程语言,一般是 Java 或者 C++,现在流行的编程语言几乎都是面向对象,面向对象的编程思想足够我们应付几乎所有的问题,即使你不了解文章要说的函数式编程也没有任何问题,也不会对我们在日常编程中解决问题有什么影响。但是理解函数式编程会在解决某些问题上显得更优雅,函数式编程和面向对象编程并不冲突,没有孰优孰劣。相反,这两种编程思想是相辅相成的,现在很多语言,例如:Java、C# 等,随着编程思想的发展,在面向对象的基础上也开始支持函数式编程,为我们解决问题提供多种方案,像 Swift 这种新兴语言从一开始就是支持面向对象和函数式编程的。所以,了解函数式编程是很有必要的。

数组 for 循环遍历

数组是 C 语言中常见的数据结构,也是我们在编程过程中常接触的,对于数组的遍历,C 语言中的方式就是使用 for 循环或者 while 循环。

#include <stdio.h>

int main() {
   int n[10];
   int i, j;

   for (i = 0; i < 10; ++i) {
      n[i] = i + 100;
   }

   for (j = 0; j < 10; ++j) {
      printf("第[%d]个 = %d\n", j, n[j]);
   }

   return 0;
}

尽管后续的一些语言中有 for-in 或者 for-of 的遍历方式,但跟 C 语言的 for 循环遍历也是差不多的,只是省去了我们从数组中取出元素的步骤,但是这样我们需要自己维护遍历索引。

using System;

class TestArray
{
    static void Main(string[] args)
    {
        int[] n = new int[10];
        for (int i = 0; i < 10; ++i)
        {
            n[i] = i + 100;
        }

        foreach (int j in n)
        {
            int i = j - 100;
            Console.WriteLine("第[{0}]个 = {1}", i, j);
        }
        Console.ReadKey();
    }
}

这种 C 语言风格的数组遍历方式是我们操作数组元素的基础,如果我们需要对数组中的某些元素进行操作,我们也会遍历数组中所有的元素,在进行操作之前先判断是否符合要求,不符合要求则用 continue 跳过。考虑数组的操作一直都是停留在数组元素上,没有从数组的整体来思考。函数式编程会从更高的层次来考虑,把数组当作整体,我们对数组元素的判断其实就是对数组进行过滤,然后再对过滤后的数组进行操作,函数式编程将这些数组常用的操作封装起来,让我们从另一种角度操作集合。

函数式操作集合

在上文有提到过,函数式操作集合是从整体上考虑的,这种方式不会聚焦于集合中的元素。它将我们对集合的常用操作抽象出来,例如:过滤、遍历、转换等,分别对应的函数就是 filter/forEach/map,这里使用的是常用的函数名,一些语言有相同作用的函数,但是名称可能会不一样。

这里举一个例子,将数组中为双数的数字翻倍之后打印出来

C 语言风格的遍历

let nums: Array<number> = [1, 2, 3, 4, 5];
for (let i = 0; i < nums.length; ++i) {
    let num = nums[i];
    if (num % 2 != 0) {
        continue;
    }
    num *= 2;
    console.log(`number: ${num}`);
}

上面的代码是很典型的 C 语言风格的遍历,可以看到我们是针对于单个元素,在遍历中取出单个元素,然后进行需求中的操作。这种方式很符合我们的直觉,在很长一段时间里,我都是以这种方式来处理集合的,集合拿到手上不管三七二十一,先执行遍历,随后在对单个元素执行我们想要的操作。

函数式风格

let nums: Array<number> = [1, 2, 3, 4, 5];
nums.filter(num => num % 2 == 0)
    .map(num => num * 2)
    .forEach(num => console.log(`number: ${num}`));

函数式的风格相较于 C 语言的风格,一眼看上去没有那么好理解,其实是非常简单的。在 C 语言风格执行的 continue 操作,就相当于函数式编程中的过滤,对应到例子上就是过滤出双数,然后执行转换操作,对应到例子上就是将双数翻倍,然后遍历翻倍后的计算结果打印出来。

当熟悉了集合常用的操作函数,会发现这种方式对集合问题的抽象更为合理,并且更为简洁,尽管 C 语言风格更为直观,但是程序员就应该用更合理、更优雅的方式来解决问题。以 TypeScript 语言为例,TS 还为集合提供了其他有用的操作,例如 reduce/find/findIndex/some/every。

我们把上面的例子扩展一下,将数组中为双数的数字翻倍之后相加,打印相加之后的数值

C 语言风格

let nums: Array<number> = [1, 2, 3, 4, 5];
let sum = 0;
for (let i = 0; i < nums.length; ++i) {
    let num = nums[i];
    if (num % 2 != 0) {
        continue;
    }
    sum += num * 2;
}
console.log(`number: ${sum}`);

在 C 语言的风格里我们需要额外定义一个变量 sum 来保存上次遍历相加后的数值。

函数式风格

let nums: Array<number> = [1, 2, 3, 4, 5];
let sum = nums.filter(num => num % 2 == 0)
    .map(num => num * 2)
    .reduce((pre, cur) => pre + cur);
console.log(`number: ${sum}`);

函数式的方式把每一步区分的非常明显,我们可以任意增加和删减其中一步。得益于 TS 箭头函数的支持,相较于 C 语言风格的实现更为整洁。

总结

当然函数式编程能做的事情非常多,这里介绍的只是集合函数式编程的基础,语言给我们提供了好用的工具,如何组合这些工具去解决问题,是我们在实际编程中要去思考的。当然,函数式编程不是万能的,它只是针对于某些问题有更好的解决方式。

也许有人会说不用函数式也能解决问题,函数式编程没啥用。但是作为程序员的我们,学习如何把需求更优雅的解决,不是应该的吗,毕竟需求总是在变,代码总会被修改。

写好代码应该是每一个程序员的追求。正如诗人对写出好诗的追求,画家对画出好画的追求。