函数式编程真的像人们吹捧的那样好吗?
你可能听说过函数式编程(以下简称FP)。有人说它能赋予你超能力,也有人认为它偏离了面向对象编程(以下简称OOP)的方向。有些文章只会简单介绍map、filter和reduce,而有些文章则会抛出诸如函子、单子和代数数据类型之类的晦涩术语。那么,我们究竟为什么要费心研究函数式编程呢?
太长不看
- 不可变性约束促进了松耦合模块化代码库的构建,这种代码库更容易单独理解。因此,代码的可维护性得到了提高。
- 函数式编程范式非常重视抽象,将其视为实现 DRY 代码和表达精确定义的强大工具。
- 许多抽象概念已经为我们定义好了,使我们能够编写声明式代码。这些抽象概念基于数十年的数学研究。
- 原则上,解耦代码能够实现并行执行,从而充分利用多核和分布式系统的计算资源,提升性能。然而,大多数 JavaScript 实现无法从这一原则中获益,并且缺乏函数式编程所依赖的多种优化策略。
- 函数式编程 (FP) 和面向对象编程 (OOP)都认同共享可变状态是不好的,抽象是好的。OOP 试图通过减少共享的内容来处理共享可变状态,而 FP 则完全不允许可变性。这两种方法看似通往截然不同的世界,但实际上都是为了通过不同的模式来管理代码的复杂性。根据你对 OOP 和 FP 的定义,两者的一些特性可以结合使用。
代码维护
程序很快就会发展到难以理解其功能或运行方式的地步。如果程序没有被拆分成更小的部分,这种情况尤其如此。理解程序需要同时追踪所有运行的组件。计算机非常擅长处理这类任务,但我们人类的大脑一次只能存储一定量的信息。
程序可以分解成若干小部分,这些小部分组合起来完成更大的任务,但必须特别注意确保这些小部分之间不存在隐式依赖关系。隐式依赖关系的最大来源是共享的可变状态。函数式编程认识到这是复杂性的危险来源,它会导致难以追踪的错误。函数式编程的核心原则是不允许任何形式的修改。
仔细想想。如果不允许修改变量,你的编程方式会有什么改变?你将无法使用 for 循环或 while 循环,因为它们都依赖于变量状态的改变。你学过的所有那些用于原地排序数组的复杂算法都失效了,因为数组一旦定义就不能再修改。那我们该如何完成任何任务呢?
如果你学习的是传统的命令式编程,那么学习函数式编程可能会让你感觉像是走错了方向。为了避免可变性而要经历这么多繁琐的步骤,真的值得吗?在很多情况下,答案是肯定的。代码模块化和松耦合是编程的理想,它们一次又一次地证明了自身的重要性。本系列文章的其余部分将主要探讨如何应对不可变性的限制。
抽象
抽象的本质在于发现共同模式,并将它们归纳到精确的定义之下。我喜欢把编程比作编写一本字典。一个词的定义是由其他一些被假定为已经理解的词构成的。(我以前最讨厌翻我妈那本老旧的韦氏词典,因为里面的定义用了太多词,以至于我费尽周折找到所有需要知道的词之后,反而忘了自己最初要查的是哪个词。)
依赖先前的定义实际上包含两个强大的概念:特殊形式和词法作用域。词法作用域简单来说就是我们可以引用已经定义过的事物。特殊形式可以通过一个例子更好地解释。假设我让你在+不使用 JavaScript 内置运算+符的情况下定义数字运算符。这是不可能的(除非你也自己定义数字)。这是因为+内置运算符是一种特殊形式,它被认为是基础知识,所以你可以在其他定义中使用它。
那么,这一切与抽象有什么关系呢?说实话,这有点跑题了,但重点在于精确的定义至关重要。函数式编程(FP)作为一种编程范式,非常重视恰当的抽象。你可能听说过“不要重复自己”(DRY)原则。抽象正是实现这一原则的工具。每当你定义一个常量来代替字面值,或者将一个过程组合成一个函数时,你就是在运用抽象的力量。
声明式与命令式
你可能听说过声明式代码好,而命令式代码则差一些。声明式代码描述的是“发生了什么”,而不是“如何去做”。关键在于:总得有人编写实际执行操作的代码。任何声明式代码背后都隐藏着执行繁重工作的命令式代码,这些代码可能在程序集、编译器、库或SDK层面实现。如果你编写的代码会被其他人调用,那么创建声明式接口就至关重要,但要正确编写这些接口并非易事。幸运的是,许多才华横溢的人花费数十年时间完善抽象概念,使我们不必为此费心。
在本系列的下一篇文章中,我们将探讨 ` mapand` 和 ` filterarray` 方法reduce。这三种方法是源自范畴论(数学本身的数学)的强大抽象。结合定义明确且命名恰当的函数,这三种方法可以生成丰富的声明式代码,这些代码通常几乎可以像自描述语句一样阅读。
表现
还记得不可变性约束如何减少依赖关系,使我们能够独立理解代码吗?事实证明,这也意味着机器可以独立运行代码。这意味着我们可以充分利用多核计算机或分布式计算的强大功能。由于处理器速度的提升空间有限,并行执行的能力变得越来越重要。
遗憾的是,现代计算实际上需要在机器层面实现可变性。函数式编程语言依赖于持久化数据结构、惰性求值和尾调用优化等概念来实现高性能。大多数现代浏览器中的 JavaScript 实现并不支持这些特性。(令人惊讶的是,在所有浏览器中,Safari 是唯一实现了尾调用优化的浏览器。)
所以这既是好消息也是坏消息。函数式编程风格的代码可以轻松并发运行,这非常棒。但对我们 JavaScript 程序员来说,性能并非函数式编程的优势。我认为在很多情况下,性能本身并不是 JavaScript 的优势,但如果你不得不使用 JavaScript,并且必须榨干代码的每一分性能,那么函数式编程可能并不适合你。
与面向对象编程的比较
现在来点轻松的。说实话,我对面向对象编程(OOP)了解不多,所以我会以这篇入门介绍作为指导。那么,关键问题来了:函数式编程(FP)和面向对象编程(OOP)哪个更好?
正如你可能已经猜到的,这并不是一个特别有用的问题。这很大程度上取决于你对函数式编程(FP)和面向对象编程(OOP)的定义。我们先从共同点说起。FP 和 OOP 都认同共享可变状态是不好的,抽象是好的。这两种范式都是为了更好地维护代码而发展起来的。它们的区别在于,FP 通过避免可变性来避免共享可变状态,而 OOP 则通过封装来避免共享。
沿着这种二分法的两条分支,你会进入两个看似截然不同的世界。面向对象编程(OOP)针对各种涉及有限共享的复杂场景,提供了数十种设计模式;而函数式编程(FP)则运用大量源自范畴论的晦涩术语来应对不可变性约束。从这个角度来看,这两个世界开始变得非常相似。OOP 一如既往地使用工厂和适配器等现实世界的类比来描述不同的策略,而 FP 则更倾向于使用直接取自范畴论数学术语的精确词汇。
我们可以取长补短,将面向对象编程 (OOP) 和函数式编程 (FP) 的优点结合起来使用。我个人认为,以不鼓励可变性的函数式编程为基础是最佳的起点。你有没有想过,能否创建一组 OOP 基类,并以此定义一切?我想,如果你尝试这样做,你会发现要封装世界上所有数据并不现实,但你肯定可以找到一些基本行为,这些行为或多或少是基础性的。随着你定义这些可以组合起来定义更复杂行为的接口,你的定义很可能会变得非常抽象和数学化。
一些函数式编程(FP)的支持者可能不愿承认,但像函子、幺半群和单子这样的代数结构本质上等同于面向对象编程(OOP)中的接口。然而,这些接口永远不会被继承,而是始终需要实现。您是否知道,JavaScript 中存在一套规范,规定了如何将这些代数结构实现为对象方法?正是由于这套规范,您可以受益于一系列彼此兼容的声明式库,这些库允许您在 JavaScript 中使用对象方法链来实现函数式编程的操作。
结论
函数式编程彻底改变了我对编程的看法。诚然,由于 JavaScript 的性能缺陷,它的应用范围确实有限,但我很喜欢它为我构建了许多实用的抽象层,让我能够编写更易于维护的声明式代码。希望你现在也能体会到这种编程范式的价值。如果你有任何疑问,或者对本文有任何不同意见,请随时告诉我!
文章来源:https://dev.to/sethcalebweeks/is-functioning-programming-worth-the-hype-pragmatic-javascript-series-no