3

渐渐地我们所熟悉的语言基本都或多或少地支持了函数式编程的特性,也越来越多地在各种场合听到“函数式编程”。那么究竟什么是函数式编程呢?它会对我们带来什么影响?这些是我需要去探究的。看了一些书,查了一些资料,我觉得John Hughes的Why Functional Programming Matters讲得最高屋建瓴。本文的核心观点和叙述架构基本上来自这篇文章。

什么是函数式编程

函数式编程,顾名思义,在它的世界中,程序软件就是一系列对参数进行操作的函数,这是函数式编程的思想基石。例如我们一个普通的程序入口就是一个接受一些参数的main函数,而它本身也是由一些函数组成,而这些函数也是由更小的函数组成,一直到最简单的函数。你看,我们可以从函数的角度去构建整个软件。
所以函数式编程是一种编程范式,不在于具体的语言,具体的API。

函数式编程的显著特征 - 不可变|无副作用|引用透明

在函数式编程中,一个变量一旦被赋值,是不可改变的。没有可变的变量,意味着没有状态。而中间状态是导致软件难以管理的一个重要原因,尤其在并发状态下,稍有不慎,中间状态的存在很容易导致问题。没有中间状态,也就能避免这类问题。无中间状态,更抽象地说是没有副作用。说的是一个函数只管接受一些入参,进行计算后吐出结果,除此以外不会对软件造成任何其他影响,把这个叫做没有副作用。因为没有中间状态,因此一个函数的输出只取决于输入,只要输入是一致的,那么输出必然是一致的。这个又叫做引用透明。这些不同的名词差不多都在讲一个意思。

函数式编程的目标 - 模块化

我们需要透过表象看到更深的抽象层次,例如结构化编程和非结构化编程的区别,从表面上看比较大的一个区别是结构化编程没了“goto”语句。但更深层次是结构化编程使得模块化成为可能。像goto语句这样的能力存在,虽然会带来一定的便利,但是它会打破模块之间的界限,让模块化变得不容易。而模块化有诸多好处,首先模块内部是更小的单一的逻辑,更容易编程;其次模块化有利于复用;最后模块化使得每个模块也更加易于测试。模块化是软件成功的关键所在,模块化的本质是对问题进行分解,针对细粒度的子问题编程解决,然后把一个个小的解决方案整合起来,解决完整的问题。这里就需要一个机制,可以将一个个小模块整合起来。函数式编程有利于小模块的整合,有利于模块化编程。

将函数整合起来 - 高阶函数(Higher-order Functions)

高阶函数的定义。满足以下其中一个条件即可称为高阶函数:

  • 接受一个或者多个函数作为其入参(takes one or more functions as arguments)
  • 返回值是一个函数 (returns a function as its result)

假如我们需要计算出学校中所有女生的成绩,和所有女老师的年龄。传统的编程方式我们是这样做的:

//求所有女生的成绩
//1\. 定义一个列表,用来存放所有女生的成绩
List<Integer> grades = new ArrayList();
//2\. 遍历找出所有女生
for (Student s : students) {
    if (s.sex.equals("femail")) {
        //3\. 获取该女生的成绩
        int grade = s.grade.
        grades.add(grade);
    }
}

//求所有女老师的年龄
//1\. 定义一个列表,用来存放女老师的年龄
List<Integer> ages = new ArrayList();

//2\. 遍历找出所有女老师
for (Teacher t : teachers) {
    if (t.sex.equals("femail")) {
        //3\. 获取女老师的年龄
        ages.add(t.age);
    }
}

用函数式编程的方式求解,可以这样做:

//求所有女生的成绩
List<Integer> grades = students.stream().filter(s -> s.sex.equals("femail")).map(s -> {return s.grade}).collect(Collectors.toList());

//求所有女老师的年龄
List<Integer> ages = teachers.stream().filter(t -> t.sex.equals("femail")).map(t -> {return t.age}).collect(Collectors.toList());

例子中使用的是比较著名的高阶函数,map, filter,此外常听到的还有reduce。这些高阶函数将循环给抽象了。map,filter里面可以传入不同的函数,操作不同的数据类型。但高阶函数本身并不局限于map,reduce,filter,满足上述定义的都可以成为高阶函数。高阶函数像骨架一样支起程序的整体结构,具体的实现则由作为参数传入的具体函数来实现。因此,我们看到高阶函数提供了一种能力,可以将普通函数(功能模块)整合起来,使得任一普通函数都能被灵活的替换和复用。

缓求值(Lazy Evaluations)

假如有一个函数g(f(x)),在常规的一旦知道x的值,则立即先求出f(x)的值,再将这个值代入到g()函数中。例如Java中写

System.out.printl("Hello " + people.name);

//编译后其实会变成
String s = "Hello " + people.name;
System.out.printl(s);

因为现在大部分传统编程语言都是及早求值(eager evaluation)的。而在缓求值中,除非g()的结果需要被用到了,g()才会被触发计算,而g()需要f()作为其输入,f()把x代入开始计算。
缓求值的好处是:

  • 使昂贵的计算到必要时才会执行,优化性能
  • 可以建立无限大集合,只要一直接到请求,就一直输出元素
  • 缓求值使得代码具备了巨大的优化潜能(例如TensorFlow用了这个思路)

但这与模块化有什么关系呢?有关系!
假如全校学生的资料存放在一个巨大的文件中,我们无法一次性将它load到内存里面,但是我们又需要知道所有国庆节生日的同学名单。
套用g(f(x))的格式,我们需要filter(readFile(f)),按照及早求值的方式,先用readFile()把文件内容读取出来,然后在filter()里面过滤,然而我们知道这个思路不可行,因为内存大小有限,无法一次性读取。基于性能考虑,我们只好用别的方式,将readFile()和filter()写在一个函数中,边读边过滤,但是这样就没有模块化了。按照函数式编程缓求值的方式,先执行到filter(),根据filter()函数的需要,readFile()去读取对应的内容,由于用多少,读多少,对内存没有压力,并且又很好地实现了两个模块的分离。

结语

看待函数式编程,如果只看到一些具体的特性,像map,reduce,缓求值等等,就会觉得不过如此,甚至觉得不过是把一些常用的逻辑整理了一下而已,那就错过了函数式编程的精彩。我们需要从函数式编程的思想基石--基于函数构建软件,以及函数式编程对于模块化的益处,我们就能看到函数式编程思想的魅力。
最后,函数式编程会颠覆面向对象编程吗?似乎蛮多人讨论的。从我的理解,面向对象依然强大,在对现实世界的抽象上无可比拟。函数式编程和面向对象编程是不同的思路,有各自适用的场景在,也不是为了互相替代,是可以共存的。从像Java这样典型的面向对象语言开始支持函数式编程的特性,到Scala,Python这的语言一开始就即支持函数式编程又支持面向对象编程,可以看出是可以共存和互补的。

参考资料


本文作者:zhang_sl

阅读原文

本文为云栖社区原创内容,未经允许不得转载。


阿里云云栖号
27.8k 声望35.7k 粉丝

阿里云官网内容平台