在本文中,我们将稍微解释一下什么是函数式编程,然后通过五种方法让您的 JavaScript 在风格上更具函数性。
什么是函数式编程? {#whatisfunctionalprogramming}
函数式编程是一种使用函数及其应用程序而不是命令编程语言中使用的命令列表的编程风格。
它是一种更抽象的编程风格,其根源在于数学------特别是被称为Lambda 微积分的数学分支,它由数学家阿隆佐丘奇于 1936 年设计,作为可计算性的正式模型。它由将一个表达式映射到另一个表达式的表达式和函数组成。从根本上说,这就是我们在函数式编程中所做的:我们使用函数将值转换为不同的值。
这篇文章的作者近几年爱上了函数式编程。我们开始使用鼓励更函数式风格的 JavaScript 库,然后通过学习如何在Haskell中编码直接跳入深层次。
Haskell 是 1990 年代开发的纯函数式编程语言,类似于 Scala 和 Clojure。使用这些语言,您被迫以函数式风格编写代码。学习 Haskel 让我们真正体会到函数式编程提供的所有优势。
JavaScript 是一种多范式语言,因为它可以用于以命令式、面向对象或函数式风格进行编程。不过,它确实特别适合函数式风格,因为函数是一流的对象,这意味着它们可以分配给变量。这也意味着函数可以作为参数传递给其他函数(通常称为回调),也可以作为其他函数的返回值。返回其他函数或接受它们作为参数的函数称为高阶函数,它们是函数式编程的基本部分。
近年来,以函数式风格编写 JavaScript 变得越来越流行,尤其是随着 React 的兴起。React 使用适合函数式方法的声明式 API,因此充分理解函数式编程的原则将改进您的 React 代码。
为什么函数式编程这么好? {#whyisfunctionalprogrammingsogood}
简而言之,函数式编程语言通常会生成简洁、清晰和优雅的代码。代码通常更容易测试,并且可以毫无问题地应用于多线程环境。
如果你与很多不同的程序员交谈,你可能会从每个人那里得到完全不同的关于函数式编程的观点------从那些绝对厌恶它的人到那些绝对喜欢它的人。我们(本文的作者)位于天平的"喜欢它"一端,但我们完全理解它并不是每个人都喜欢的,尤其是因为它与通常教授编程的方式截然不同。
然而,一旦您掌握了函数式编程的诀窍,并且一旦思路清晰,它就会成为第二天性并改变您编写代码的方式。
规则 1:净化你的函数 {#rule1purifyyourfunctions}
函数式编程的一个关键部分是确保您编写的函数是"纯"的。如果您不熟悉这个术语,纯函数基本上满足以下条件:
-
它具有参照透明性。这意味着,给定相同的参数,该函数将始终返回相同的值。任何函数调用都可以用返回值代替,程序仍将以相同的方式运行。
-
它没有副作用。这意味着该函数不会在函数范围之外进行任何更改。这可能包括更改全局值、记录到控制台或更新 DOM。
纯函数必须至少有一个参数并且必须返回一个值。如果您考虑一下,如果他们不接受任何参数,他们将没有任何数据可以使用,如果他们不返回值,那么该函数的意义何在?
纯函数可能从一开始就不是完全必要的,但是不纯函数可能会导致程序发生整体变化,从而导致一些严重的逻辑错误!
例如:
//impure
let minimum = 21
const checkAge = age => age >= minimum
//pure
const checkAge = age => {
const minimum = 21
return age >= minimum
}
在非纯函数中,checkAge
函数依赖于可变变量minimum
。例如,如果minimum
稍后要在程序中更新变量,则该checkAge
函数可能会返回具有相同输入的布尔值。
想象一下,如果我们运行这个:
checkAge(20) >> false
现在,让我们想象一下,在代码的后面,一个changeToUK()
函数将 的值更新minimum
为 18。
然后,想象我们运行这个:
checkAge(20) >> true
checkAge
现在,尽管给定了相同的输入,该函数仍会计算出不同的值。
纯函数使您的代码更具可移植性,因为它们不依赖于作为参数提供的值之外的任何其他值。返回值永远不会改变的事实使得纯函数更容易测试。
始终如一地编写纯函数也消除了发生突变和副作用的可能性。
为了让你的函数更便携,确保你的函数始终保持纯净。
规则 2:保持变量不变 {#rule2keepvariablesconstant}
声明变量是任何程序员学习的第一件事。它变得微不足道,但在使用函数式编程风格时却非常重要。
函数式编程的一个关键原则是,一旦设置了一个变量,它就会在整个程序中保持该状态。
这是展示代码中变量的重新分配/重新声明如何成为灾难的最简单示例:
const n = 10
n = 11
TypeError: "Attempted to assign to readonly property."
仔细想想, 的值n
不能同时是10
and 11
;这没有逻辑意义。
命令式编程中的常见编码实践是使用以下代码递增值:
let x = 5
x = x + 1
在数学中,这个陈述x = x + 1
是不合逻辑的,因为如果你x
从两边都减去,你会得到0 = 1
,这显然是不正确的。
因此,在 Haskell 中,您不能将一个变量赋给一个值,然后再将其重新赋给另一个值。要在 JavaScript 中实现这一点,您应该遵循始终使用const。
规则 3:使用箭头函数 {#rule3usearrowfunctions}
在数学中,函数的概念是将一组值映射到另一组值。下图显示了通过平方将左侧值集映射到右侧值集的函数:
这就是用箭头符号在数学中的写法:f: x → x²
。这意味着该函数f
将值映射x
到x²
。
我们可以使用箭头函数来几乎相同地编写这个函数:
const f = x => x**2
在 JavaScript 中使用函数式风格的一个关键特性是使用箭头函数而不是常规函数。当然,这确实归结为样式,在常规函数上使用箭头函数实际上不会影响代码的"功能"程度。
然而,在使用函数式编程风格时,最难适应的事情之一是每个函数的心态都是输入到输出的映射。没有程序之类的东西。我们发现使用箭头函数可以帮助我们更多地理解函数的过程。
箭头函数有一个隐含的返回值,这确实有助于可视化此映射。
箭头函数的结构------尤其是它们的隐式返回值------有助于鼓励编写纯函数,因为它们的字面结构是"输入映射到输出":
args => returnValue
我们喜欢强调的另一件事,尤其是在编写箭头函数时,是三元运算符的使用。如果您不熟悉三元运算符,它们是一个内联if...else
语句,形式为condition ? value if true : value if false
。
在函数式编程中使用三元运算符的主要原因之一是else
语句的必要性。如果不满足原始条件,程序必须知道该怎么做。例如,Haskell 强制执行一个else
语句,如果没有给出,将返回一个错误。
使用三元运算符的另一个原因是它们是始终返回值的表达式,而不是if-else
可用于执行具有潜在副作用的操作的语句。这对于箭头函数特别有用,因为这意味着您可以确保有一个返回值并保持将输入映射到输出的图像。如果您不确定语句和表达式之间的细微差别,这篇关于语句与表达式的指南非常值得一读。
为了说明这两个条件,下面是一个使用三元运算符的简单箭头函数的示例:
const action = state => state === "hungry" ? "eat cake" : "sleep"
该action
函数将根据参数的值返回"吃"或"睡"的值state
。
因此,总结一下:在让你的代码更具功能性的时候,你应该遵循这两条规则:
-
使用箭头符号编写函数
-
用三元运算符替换
if...else
语句
规则 4:删除 For 循环 {#rule4removeforloops}
鉴于使用for
循环来编写迭代代码在编程中非常普遍,所以说要避免它们似乎很奇怪。事实上,当我们第一次发现 Haskell 甚至没有任何for
循环操作时,我们很难理解一些标准操作是如何实现的。然而,循环没有出现在函数式编程中有一些很好的理由for
,我们很快发现每种类型的迭代过程都可以在不使用for
循环的情况下实现。
不使用循环的最重要原因for
是它们依赖于可变状态。让我们看一个简单的sum
函数:
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
如您所见,我们必须let
在for
循环本身中使用 a ,并且还用于我们在for
循环中更新的变量。
如前所述,这在函数式编程中通常是不好的做法,因为函数式编程中的所有变量都应该是不可变的。
如果我们想编写所有变量都不可变的代码,我们可以使用递归:
const sum = n => n === 1 ? 1 : n + sum(n-1)
如您所见,没有任何变量被更新。
我们中间的数学家显然知道所有这些代码都是不必要的,因为我们可以使用 的漂亮求和公式0.5*n*(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:避免类型强制 {#rule5avoidtypecoercion}
当使用不需要类型声明的语言(如 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 中,语句中的值
0
等同if
于false
. 这可能导致懒惰的编程技术,忽略检查数值数据是否等于0
。
例如:
const even = n => !(n%2)
这是一个评估数字是否为偶数的函数。它使用!
符号将 的结果强制n%2 ?
转换为布尔值,但 的结果n%2
不是布尔值,而是一个数字(或者0
或1
)。
像这样的 hack,虽然看起来很聪明并减少了您编写的代码量,但打破了函数式编程的类型一致性规则。因此,编写此函数的最佳方式如下:
// even :: Number -> Number
const even = n => n%2 === 0
另一个重要的概念是确保数组中的所有数据值都属于同一类型。这不是由 JavaScript 强制执行的,但是当您想要使用高阶数组方法时,不同的类型会导致问题。
例如,product
将数组中的所有数字相乘并返回结果的函数可以使用以下类型声明注释编写:
// product :: [ Number ] -> Number
const product = numbers => numbers.reduce((s,x) => x * s,1)
在这里,类型声明清楚地表明函数的输入是一个包含 type 元素的数组Number
,但它只返回一个数字。类型声明清楚地表明了该函数的输入和输出是什么。显然,如果数组不仅仅包含数字,则此函数将无法工作。
Haskell 是一种强类型语言,而 JavaScript 是弱类型语言,但是为了使您的 JavaScript 更具功能性,您应该在声明函数之前编写类型声明注释,并确保避免使用类型强制快捷方式。
我们还应该在这里提到,如果你想要一个强类型的 JavaScript 替代品来强制类型一致性,你显然可以转向TypeScript。
结论 {#conclusion}
总而言之,以下是帮助您实现功能代码的五个规则:
-
保持你的功能纯净。
-
始终使用const声明变量和函数。
-
对函数使用箭头符号。
-
避免使用
for
循环。 -
使用类型声明注释并避免类型强制快捷方式。
虽然这些规则不能保证您的代码是纯函数式的,但它们将大大有助于使其更具功能性,并有助于使其更简洁、清晰和更易于测试。
我们真心希望这些规则能像帮助我们一样帮助您!我们都是函数式编程的忠实粉丝,我们强烈鼓励任何程序员使用它。