【玩转 JS 函数式编程_001】第一章 何谓函数式编程

发布于:2024-10-12 ⋅ 阅读:(134) ⋅ 点赞:(0)

第一章 入门函数式编程的若干问题 Becoming Functional – Several Questions


函数式编程(Functional programming,或 FP) 早在计算机诞生伊始就已经存在了。由于它越来越多地应用于很多主流框架和工具库,当下的函数式编程可以说正处在某种意义上的“文艺复兴”时期,尤其是在 JavaScript 领域。

本章主要内容有:

  1. 介绍函数式编程的相关概念,对其有个初步认识;
  2. 函数式编程带来的好处(及问题),以及为什么应该用好它;
  3. 考察 JavaScript 适合践行函数式编程的原因;
  4. 回顾必要的 JS 特性与常用工具,以全面掌握本书各知识点。

学完本章,您将习得本书后续章节将会用到的一些基本工具。首先来了解一下函数式编程。

1.1 何谓函数式编程 What is functional programming?

回顾一下计算机发展史,您会发现现行在用的第二古老的编程语言 Lisp,就是以函数式编程为基础的。此后,函数式编程语言层出不穷,函数式编程也得到了更为广泛的应用。可即便如此,如果问大家什么是函数式编程,您可能会得到两种截然不同的回答。

小知识

对于喜欢打破砂锅问到底的人或者热衷于搜集历史掌故的发烧友而言,目前仍在使用的最古老的语言是 Fortran,它于 1957 年问世,比 Lisp 还要早一年。在 Lisp 之后不久,又出现了另一种长寿语言 COBOL,用于面向业务编程(business-oriented programming)。

取决于询问的人,您要么听到这样的回答:函数式编程是一种现代的、先进的、开明的编程手法,其他编程范式在它面前都会黯然失色;要么听到另一种声音:函数式编程基本上还是一个理论的产物,其复杂程度超过了带来的好处,在现实世界中几乎不可能实现。实际上,真正的答案,按照惯例,并不是非黑即白地走极端,而是介于这两者之间。我们不妨从理论和实践的对比出发,看看究竟该如何规划函数式编程的使用。

1.1.1 对比理论与实践 Theory versus practice

本书不会对函数式编程作理论上的深入探讨,而是重点展示如何将函数式编程的一些技术和原则成功应用于日常的 JavaScript 编程中。本书也不会教条式地行文,而是从实用的角度展开各个话题。我们既不会因为某些有用的 JavaScript 构造(constructs)不符合函数式编程的学术要求而绕开它们,也不会仅仅为了迎合函数式的编程范式而避开一些实用的 JavaScript 特性。事实上,本书毋宁说是在介绍 类函数式编程SFP, Sorta Functional Programming),因为书中的代码其实是这三者的有机结合:函数式编程特性、经典的 命令式编程风格、以及 面向对象编程(object-oriented programming, 即 OOP) 的编程范式。

即便如此,本书也并非将所有的理论知识都搁置一旁。书中会有侧重地论述函数式编程的主要观点,介绍一些相关术语和基本定义,并对函数式编程的核心概念作重点阐释。我们将始终秉持“能写出务实高效的 JavaScript 代码”的初衷,不会一味去追求那些晦涩难懂的、古板教条式的函数式写法。

面向对象编程(OOP)向来是解决大型应用与系统开发的固有复杂性、以及构建简洁清爽、高可扩展(extensible)、且缩放自如(scalable)的应用框架的一种行之有效的解决方案。然而,随着当下 Web 应用的规模不断扩展,几乎所有的项目代码,其背后的复杂度也跟着水涨船高;此外,不断推陈出新的 JavaScript 语言特性,使得几年前还遥不可及的功能实现变成了可能。例如,使用 IonicApache CordovaReact Native 开发的各类移动端(混合)应用;再比如使用 ElectronTauriNW.js 1等产品开发的各种桌面端应用等等。时至今日,JavaScript 还通过 Node.jsDeno 2 等运行时产品成功进军后端开发,其适用范围已在很大程度上得到扩展,以应对现代设计所带来的额外复杂性。

1.1.2 换一种方式思考 A different way of thinking

函数式编程是一种不同的编程方式,学起来可能还会比较吃力。在大多数语言中,编程往往都是命令式的。程序是以规定方式执行的一系列语句,通过创建并操作对象(objects)来实现预期的结果,这通常意味着修改对象本身。而函数式编程(FP)则是基于表达式求值来得到想要的结果。这些表达式由组合在一起的函数 (functions)构建而成。在函数式编程中,像函数传递(pass functions around,例如将函数作为另一个函数的参数传入,或者将一个函数作为原函数的计算结果返回)、不使用循环(not use loops,转而使用递归)、以及跳过副作用(skip side effects,例如修改对象或全局变量本身)等等,都是司空见惯的操作。

换言之,函数式编程聚焦的是实现的 是什么what should be done),而非 怎样 实现(how it should be done)。您无需关心具体的循环或数组,而是站在更高层面上考虑要完成的任务是什么。一旦适应了这种编程风格,您的代码将变得更为简洁与优雅,并且易于测试及后续调试。但是,也不要落入将函数式编程视为终极奋斗目标的陷阱里。与其他软件工具一样,函数式编程也仅仅是达成目的的一种手段而已。有时候函数式风格的代码未必就是好代码。正如任何技术一样,用函数式编程的思想写出糟糕的代码也是极有可能的!

1.1.3 函数式编程与其他编程范式 FP and other programming paradigms

编程范式依照编程语言具备的特性对其进行分类。但某些语言可能会被归到多种编程范式下 —— 比如 JavaScript 就是个典型代表。

编程范式的一个重要分类,体现在 命令式(imperative 语言和 声明式(declarative 语言的对立统一上。使用命令式语言的开发者必须分步骤逐一指示计算机该如何完成相应的工作。这样的编程风格既可以是面向过程编程(be precedural),其指令是按过程来分组呈现的;也可以是面向对象(object-oriented)编程,其指令则是按对象的相关状态进行分类组织。

而对于声明式语言,开发者只需声明最终结果必须满足的条件即可,无需关注具体的实现细节。声明式语言的编程风格既可以是逻辑式的(logic-based,即基于逻辑规则与约束),也可以是响应式的(reactive,即基于数据及事件流),亦或是函数式的(functional,即基于函数的应用与组合)。从某种意义上将,我们可以说命令式语言关注的是 过程(how,而声明式语言关注的是 结果(what

JavaScript 无疑是多范式语言:它既支持命令式编程(面向过程与面向对象均可),同时也支持声明式编程,包括函数式编程(正如本书将要介绍的所有主要内容;本书第 5 章将重点讨论),以及响应式编程(在第 11 章讲设计模式的实现时还会重点介绍)。

为了演示命令式编程与声明式编程在解决问题时的差异,来看下面这个基本示例:如下所示,假设有一组包含个人信息的数组:

// imperative.js
const data = [
  { name: "John", age: 23, other: "xxx" },
  { name: "Paul", age: 18, other: "yyy" },
  { name: "George", age: 16, other: "zzz" },
  { name: "Ringo", age: 25, other: "ttt" },
];

假如要提取成年人的数据信息(年龄至少为 21 岁)。若采用命令式编程,可以这么写:

// 接上一段代码...
const result1 = [];
for (let i = 0; i < data.length; i++) {
  if (data[i].age >= 21) {
    result1.push(data[i]);
  }
}

上述代码中,必须为目标人群初始化一个结果数组(result1),然后必须指定一个循环结构,说明索引变量(i)怎样初始化、怎样检验大小、以及完成更新。在历次循环中,还得检查对应人员的年龄,并将符合条件的人员信息推送到结果数组中。换句话说,您必须逐一指定代码必须完成的所有操作。

而采用声明式编程,则多半会这么写:

// declarative.js
const isAdult = (person) => person.age >= 21;
const result2 = data.filter(isAdult);

第一行声明了判定该人员是否为成年人的检测方法;第二行则表示答案是筛选数组后的结果,选中的是那些满足给定筛选条件的元素项(对于 isAdult() 方法,这里用的是箭头函数,本章后续还会进一步介绍)。您无需初始化结果数组,也无需指定循环方式,或者确保数组索引不会越界等等——所有这些细节都交给语言本身进行处理,不劳您操心了。

想要读懂命令式编程书写的代码,需要对编程语言本身以及循环的算法、技术等有一定的了解;声明式的代码通常更简短、更易于维护,且可读性也更强。

1.1.4 函数式编程不是什么?What FP is not

既然前面已经花了很多篇幅来谈论函数式编程(FP)是什么,这里有必要澄清一些常见的误区,看看函数式编程 不是什么——

  • 函数式编程并不仅仅是学术界的理论研究:函数式编程所赖以发展的 λ 算子(the lambda calculus),自 1936 年 阿隆佐·丘奇(Alonzo Church 提出以来,一直是理论计算机科学领域(比现代计算机语言的出现早了 20 多年)用于证明重要成果的有力工具;而如今的函数式编程语言已经在各种系统比比皆是;3
  • 函数式编程并非面向对象编程的对立面:声明式与命令式风格不必非得二选一,也可以根据实际需要搭配使用。这一点本书将贯穿始终,并融入各个领域的最佳实践。
  • 函数式编程学起来也不是那么复杂:一些函数式编程语言与 JavaScript 不太一样,但这些差异主要体现在语法层面。一旦掌握了基本概念,您就会发现从 JavaScript 中也能习得函数式编程的精髓,可谓殊途同归。

或许值得一提的,还有几个主流的现代框架,如 ReactRedux 的组合,就体现了函数式编程的思想。

举个例子,在 React 中,视图(即用户在某一时刻看到的任意内容)被认为是当前状态的一个函数。您会通过一个函数来计算每个时刻必须生成的 HTMLCSS,以黑匣子的方式来考虑问题。

同理,在 Redux 中,也有个操作(actions)的概念,会交由 reducer 进行处理。一个动作提供一些数据,而 reducer 则是一个函数,它以函数式编程的方式从当前状态和提供的数据中生成应用的新状态。

因此,鉴于理论上的优势(下一节重点介绍)与实践中的好处(例如能够使用最新的框架和工具库),考虑函数式编程都是很有意义的。您准备好了吗?


  1. NW.js(原名 Node-Webkit)是一款近年来广受关注的知名开源框架,能够通过 JavaScriptHTMLCSS 来构建桌面级应用。截止发文时,其在 GitHub 上的点赞数已突破 40.3k。 ↩︎

  2. Deno/ˈdiːnoʊ/,发音为 dee-no)是一款现代的 JavaScriptTypeScript 运行时(runtime)环境开源产品,旨在提供比 Node.js 运行时环境更安全、更简洁的开发体验。它由 Node.js 的创始人 Ryan Dahl 开发,并于 2018 年首次发布,旨在解决 Node.js 中的一些设计缺陷,截止发文时,其在 GitHub 上的点赞数已突破 94.6k。 ↩︎

  3. 这一点旨在强调函数式编程历久弥新。 ↩︎