我对函数式编程的理解

简介: 渐渐地我们所熟悉的语言基本都或多或少地支持了函数式编程的特性,也越来越多地在各种场合听到“函数式编程”。那么究竟什么是函数式编程呢?它会对我们带来什么影响?这些是我需要去探究的。看了一些书,查了一些资料,我觉得John Hughes的[Why Functional Programming Matters](https://www.cs.kent.ac.uk/people/staff/dat/mir

渐渐地我们所熟悉的语言基本都或多或少地支持了函数式编程的特性,也越来越多地在各种场合听到“函数式编程”。那么究竟什么是函数式编程呢?它会对我们带来什么影响?这些是我需要去探究的。看了一些书,查了一些资料,我觉得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这的语言一开始就即支持函数式编程又支持面向对象编程,可以看出是可以共存和互补的。

参考资料

目录
相关文章
|
程序员 Swift 开发者
26 函数式编程
函数式编程
64 0
|
2月前
|
机器学习/深度学习 数据采集 人工智能
函数式编程的实际应用
【10月更文挑战第12天】 函数式编程作为一种编程范式,在数据处理、金融、科学计算、Web 开发、游戏开发、物联网、人工智能等多个领域有着广泛应用。本文通过具体案例,详细介绍了函数式编程在这些领域的实际应用,展示了其在提高效率、确保准确性、增强可维护性等方面的显著优势。
119 60
|
18天前
|
数据采集 并行计算 算法
函数式编程
函数式编程是一种编程范式,它将计算视为数学函数的求值,并避免改变状态和可变数据。其核心思想是使用纯函数,减少副作用,提高代码的可读性和并行处理能力。
|
28天前
|
SQL 前端开发 测试技术
对函数式编程的深入理解
【10月更文挑战第25天】函数式编程提供了一种不同的编程思维方式,具有诸多优点,如提高代码质量、便于并发和并行编程、易于测试等。然而,它也存在一些局限性,需要根据具体的项目需求和场景来选择是否采用。随着对函数式编程的理解和应用的深入,它在现代软件开发中扮演着越来越重要的角色,为开发者提供了更多的编程选择和可能性。
13 1
|
2月前
|
并行计算 安全 数据处理
函数式编程和面向对象编程有什么区别?
【10月更文挑战第12天】 函数式编程与面向对象编程是两种不同的编程范式。前者强调数学函数的求值、不可变数据和纯函数,后者则以对象为核心,封装数据和方法。函数式编程更关注数据转换和计算过程,而面向对象编程关注对象的定义和交互。两者在数据处理、函数角色、代码结构、并发处理、灵活性和适用场景等方面存在显著差异。在实际开发中,可以根据需求选择合适的编程范式或结合使用。
62 4
|
4月前
|
自然语言处理 并行计算 大数据
什么是函数式编程
【8月更文挑战第2天】什么是函数式编程
114 13
|
Oracle JavaScript Java
函数式编程与Lambda表达式
函数式编程与Lambda表达式
|
安全 Java 数据库
Lambda表达式和函数式编程
Lambda表达式和函数式编程
190 4
Lambda表达式和函数式编程
|
并行计算 JavaScript 数据可视化
快速了解函数式编程
快速了解函数式编程
132 0
快速了解函数式编程
|
Scala 索引 Python
第5章 函数式编程
第5章 函数式编程
513 0
第5章 函数式编程