在这篇文章中,我们将简要介绍函数式编程的概念,然后探讨五种使你的JavaScript更符合函数式编程风格的方法。
什么是函数式编程?
函数式编程是一种编程风格,它使用函数及其应用而不是命令列表来进行编程,这与命令式编程语言不同。
函数式编程是一种更抽象的编程风格,它起源于数学,特别是数学中的一种称为 Lambda演算 的分支。Lambda演算 是由数学家阿隆佐·邱奇于1936年设计的一种形式模型,用于描述计算过程。函数式编程由表达式和将一个表达式映射到另一个表达式的函数组成。从根本上说,这就是函数式编程的本质:我们使用函数将值转换为不同的值。
作者们在近年来深深迷恋上函数式编程。我们开始使用鼓励函数式风格的JavaScript库,然后进一步学习如何在Haskell中编程。
Haskell是一种纯函数式编程语言,于1990年代开发,类似于Scala和Clojure。使用这些语言,你被迫以函数式风格编程。学习Haskell让我们真正意识到函数式编程提供的所有优势。
JavaScript是一种多范式语言,可以用来以命令式、面向对象或函数式风格进行编程。然而,它特别适合函数式风格,因为函数是一等公民,可以赋值给变量。这意味着函数可以作为参数传递给其他函数(通常称为回调函数),也可以作为其他函数的返回值。返回其他函数或接受其他函数作为参数的函数被称为高阶函数,它们是函数式编程的基本组成部分。
近年来,使用函数式风格来编程JavaScript变得越来越流行,特别是随着React的兴起。React使用一种声明式的API,非常适合函数式的方法,因此深入理解函数式编程原则将改善你的React代码。
为什么函数式编程如此优秀?
简而言之,函数式编程语言通常会产生简洁、清晰和优雅的代码。这种代码通常更易于测试,并且可以在多线程环境中无问题地应用。
如果你和许多不同的程序员交谈,你可能会得到完全不同的关于函数式编程的意见,从那些绝对讨厌它的人到那些绝对喜欢它的人都有。我(本文的作者)是属于“喜欢它”的那一派,但我们完全理解它并不适合每个人,特别是因为它与传统的编程教学方法非常不同。
然而,一旦你掌握了函数式编程,并且一旦思维方式一致,它就会变得如同第二天性,改变你编写代码的方式。
规则1:纯化你的函数
函数式编程的关键之一是确保你编写的函数是“纯粹”的。如果你对这个术语不熟悉,纯函数基本上满足以下条件:
- 具有引用透明性。这意味着给定相同的参数,函数总是返回相同的值。任何函数调用都可以用返回值替换,程序仍然能够以相同的方式运行。
- 没有副作用。这意味着函数不会在函数作用域之外做任何更改。这里包括更改全局值、向控制台打印日志或更新DOM。
纯函数必须至少有一个参数,并且必须返回一个值。如果你仔细考虑一下,如果它们不接受任何参数,它们就没有任何数据可以处理,如果它们不返回任何值,函数的存在又有什么意义呢?
纯函数一开始可能看起来并不是必需的,但使用非纯函数可能导致整个程序的变化,从而产生严重的逻辑错误!
例如:
//非纯函数
let minimum = 21
const checkAge = age => age >= minimum
//纯函数
const checkAge = age => {
const minimum = 21
return age >= minimum
}
在这个非纯函数中,checkAge函数依赖于可变的变量minimum。例如,如果minimum变量在程序的后面被更新,checkAge函数可能会以相同的输入返回一个不同的布尔值。
想象一下如果我们运行这个例子:
checkAge(20) >> false
现在,让我们想象一下,在代码的后面部分,一个名为changeToUK()的函数将minimum的值更新为18。
然后,我们来运行这段代码:
checkAge(20) >> true
现在,尽管输入相同,函数checkAge的计算结果却不同。
纯函数使得代码更具可移植性,因为它们不依赖于作为参数提供的任何其他值。返回值从不改变使得纯函数更容易进行测试。
始终编写纯函数还可以消除发生突变和副作用的可能性。
在函数式编程中,突变是一个很大的警告信号。如果你想了解更多原因,可以阅读《JavaScript变量赋值和突变指南》。
为了使函数更具可移植性,请确保始终保持函数的纯洁性。
规则2:保持变量不变
声明变量是任何程序员学习的最基本的事情之一。虽然它变得平凡,但在使用函数式编程风格时非常重要。
函数式编程的一个关键原则是,一旦变量被设定,它在整个程序中保持不变。
下面是一个最简单的例子,展示了代码中重新赋值/重新声明变量可能导致灾难的情况:
const n = 10
n = 11
TypeError: "Attempted to assign to readonly property."
如果你仔细思考一下,变量 n 的值不能同时是 10 和 11,这在逻辑上是不合理的。
在命令式编程中,一种常见的编码习惯是使用以下代码来增加变量的值:
let x = 5
x = x + 1
在数学中,表达式 x = x + 1 是不合逻辑的,因为如果你从两边同时减去 x,你会得到 0 = 1,这显然是不正确的。
因此,在 Haskell 中,你不能将一个变量赋值为一个值,然后再重新赋值为另一个值。为了在 JavaScript 中实现这一点,你应该遵循“始终使用 const 声明变量”的规则。
规则 3:使用箭头函数
在数学中,函数的概念是将一个值集合映射到另一个值集合的概念。下面的图示展示了通过平方将左侧的值集合映射到右侧的值集合的函数:
在数学中,可以使用箭头符号表示为:f: x → x²。这意味着函数 f 将值 x 映射到 x²。
我们可以使用箭头函数几乎相同地编写这个函数:
const f = x => x**2
在 JavaScript 中,使用箭头函数而不是常规函数是使用函数式风格的关键特性。当然,这实际上归结为风格的选择,使用箭头函数而不是常规函数并不会影响你的代码有多“函数式”。
然而,当使用函数式编程风格时,最难适应的一点是将每个函数看作是输入到输出的映射。这里没有所谓的过程。我们发现使用箭头函数可以更好地理解函数的过程。
箭头函数具有隐式的返回值,这真的有助于可视化这种映射过程。
箭头函数的结构,特别是它们的隐式返回值,有助于鼓励编写纯函数,因为它们的结构实际上就是“输入映射到输出”的形式:
args => returnValue
另一个我们特别强调的方面,特别是在编写箭头函数时,是对三元运算符的使用。如果你对三元运算符不熟悉,它们是一种内联的 if...else 语句,形式为 condition ? value if true : value if false。
你可以阅读《快速提示:如何在 JavaScript 中使用三元运算符》了解更多相关内容。
在函数式编程中使用三元运算符的主要原因之一是需要 else 语句。如果原始条件不满足,程序必须知道该执行什么操作。例如,Haskell 强制要求使用 else 语句,如果没有提供 else,它将返回错误。
使用三元运算符的另一个原因是它们是表达式,总是返回一个值,而不是可能具有副作用的 if-else 语句。这在使用箭头函数时特别有用,因为它意味着你可以确保有一个返回值,并保持将输入映射到输出的形象。如果你对语句和表达式之间的微妙区别不确定,可以阅读一下关于语句与表达式的指南。
为了说明这两个条件,这里有一个使用三元运算符的简单箭头函数的例子:
const action = state => state === "hungry" ? "eat cake" : "sleep"
根据 state 参数的值,action 函数将返回 “eat” 或 “sleep” 的值。
因此,总结起来,当将代码更加函数式化时,应遵循以下两个规则:
使用箭头符号编写函数
将 if...else 语句替换为三元运算符
规则4:避免使用for循环
考虑到在编程中使用for循环来编写迭代代码非常常见,所以说要避免使用它们似乎有些奇怪。事实上,当我们第一次发现Haskell甚至没有任何形式的for循环操作时,我们曾经很难理解一些标准操作如何实现。然而,为什么函数式编程中不出现for循环有一些非常好的原因,我们很快发现可以在不使用for循环的情况下完成每种类型的迭代过程。
不使用for循环最重要的原因是它们依赖于可变状态。让我们看一个简单的求和函数的例子:
function sum(n){
let k = 0
for(let i = 1; i < n+1; i++){
k = k + i
}
return k
}
sum(5) = 15 // 1 + 2 + 3 + 4 + 5
正如你所见,我们必须在for循环本身以及在for循环内部更新的变量中使用let。
正如前面已经解释的那样,在函数式编程中,这通常是不好的做法,因为所有变量都应该是不可变的。
如果我们想要编写所有变量都是不可变的代码,我们可以使用递归:
const sum = n => n === 1 ? 1 : n + sum(n-1)
正如你所看到的,没有任何变量被更新。
我们中的数学家显然会知道,所有这些代码都是不必要的,因为我们可以使用漂亮的求和公式0.5n(n+1)。但这是展示for循环与递归之间可变性差异的绝佳方式。
然而,递归并不是解决可变性问题的唯一方法,特别是当我们处理数组时。JavaScript具有许多内置的高阶数组方法,可以在不改变任何变量的情况下遍历数组中的值。
例如,假设我们想要将数组中的每个值都加1。使用命令式的方法和for循环,我们的函数可能如下所示:
function addOne(array){
for (let i = 0; i < array.length; i++){
array[i] = array[i] + 1
}
return array
}
addOne([1,2,3]) === [2,3,4]
然而,我们可以不使用for循环,而是利用JavaScript内置的map方法来编写如下的函数:
const addOne = array => array.map(x => x + 1)
如果你之前没有接触过map函数,那么学习它们绝对是值得的,还有JavaScript的其他内置高阶数组方法,比如filter,特别是如果你对在JavaScript中进行函数式编程非常感兴趣的话。你可以在《不可变数组方法:如何编写极为清晰的JavaScript代码》中找到更多关于它们的信息。
Haskell根本没有for循环。为了使你的JavaScript更加函数式,尽量避免使用for循环,而是使用递归和内置的高阶数组方法。
规则 5:避免类型强制转换
在像 JavaScript 这样不需要类型声明的语言中进行编程时,很容易忽视数据类型的重要性。JavaScript 使用了七种原始数据类型,包括:
- Number
- String
- Boolean
- Symbol
- BigInt
- Undefined
- Null
Haskell 是一种强类型语言,需要进行类型声明。这意味着在定义函数之前,你需要明确指定输入数据和输出数据的类型,使用 Hindley-Milner 系统。
例如:
add :: Integer -> Integer -> Integer
add x y = x + y
这是一个非常简单的函数,将两个数字(x 和 y)相加。可能会觉得有点荒谬,要为每个函数(包括这样非常简单的函数)解释数据的类型,但最终它有助于说明函数的预期工作方式和返回结果。这使得代码更易于调试,尤其是当代码变得更复杂时。
类型声明遵循以下结构:
functionName :: inputType(s) -> outputType
在使用JavaScript时,类型强制转换可能是一个很大的问题,因为它提供了各种各样的技巧(甚至可能被滥用)来解决数据类型不一致的问题。以下是最常见的一些技巧以及如何避免它们:
拼接字符串"Hello" +5的结果是"Hello5",这是不一致的。如果你想将一个字符串与一个数字值进行拼接,应该写成"Hello"+String(5)。布尔表达式和0在 JavaScript 中,if 语句中的0的值相当于false。这可能导致懒惰的编程技巧,忽略了对数值数据是否等于0的检查。
例如:
const even = n => !(n%2)
这是一个用于判断一个数字是否为偶数的函数。它使用了!符号将n%2的结果强制转换为布尔值,但是n%2的结果并不是布尔值,而是一个数字(要么是0,要么是1)。
像这样的技巧,虽然看起来很聪明并且减少了编写代码的量,但它违反了函数式编程的类型一致性规则。因此,编写这个函数的最佳方法如下所示:
// even :: Number -> Number
const even = n => n%2 === 0
另一个重要的概念是确保数组中的所有数据值都是相同类型的。尽管JavaScript并没有强制执行这一点,但如果数组中的元素类型不一致,就会在使用高阶数组方法时出现问题。
例如,一个将数组中所有数字相乘并返回结果的乘积函数可以使用以下类型声明注释来编写:
// product :: [ Number ] -> Number
const product = numbers => numbers.reduce((s,x) => x * s,1)
在这里,类型声明明确表示函数的输入是一个包含类型为Number的元素的数组,但它返回一个单独的数字。类型声明清楚地说明了该函数的输入和输出的期望。如果数组不仅包含数字,显然这个函数是无法正常工作的。
Haskell是一种强类型语言,而JavaScript是弱类型语言,但为了使你的JavaScript更加函数式,你应该在声明函数之前编写类型声明注释,并确保避免类型强制转换的捷径。
在这里还应该提到,如果你想要一个强类型的JavaScript替代方案,可以转向TypeScript,它将为你强制执行类型一致性。
总结
总结一下,以下是帮助您编写函数式代码的五条规则:
- 保持函数的纯净性。
- 始终使用const来声明变量和函数。
- 使用箭头符号表示函数。
- 避免使用for循环。
- 使用类型声明注释,避免类型强制转换的捷径。
虽然这些规则不能保证您的代码是纯粹的函数式,但它们将大大提高代码的函数性,并使其更加简洁、清晰和易于测试。
我们真诚希望这些规则对您有所帮助,就像它们对我们有帮助一样!我们都是函数式编程的铁杆粉丝,并极力鼓励任何程序员使用它。
如果您想进一步深入了解函数式JavaScript,我们强烈推荐阅读《Professor Frisby's Mostly Adequate Guide to Functional Programming》,该书可以免费在线阅读。如果您想全面学习Haskell,我们推荐使用Try Haskell互动教程,并阅读免费在线阅读的优秀书籍《Learn You A Haskell For Greater Good》。