什么是函数式编程?
函数式编程是一种使用函数及其应用程序而不是命令式编程语言中使用的命令列表的编程风格。
这是一种更抽象的编程风格,其根源在于数学,特别是称为Lambda 微积分的数学分支,它是由数学家 Alonzo Church 于 1936 年设计的,作为可计算性的正式模型。它由将一个表达式映射到另一个表达式的表达式和函数组成。从根本上来说,这就是我们在函数式编程中所做的:我们使用函数将值转换为不同的值。
近年来,本文的作者爱上了函数式编程。我们开始使用鼓励更具函数式风格的 JavaScript 库,然后通过学习如何在Haskell中进行编码而直接进入深层次。
Haskell 是一种纯函数式编程语言,开发于 20 世纪 90 年代,与 Scala 和 Clojure 类似。使用这些语言,您被迫以函数式风格进行编码。学习 Haskel 让我们真正体会到函数式编程提供的所有优势。
JavaScript 是一种多范式 语言,因为它可以用于以命令式、面向对象或函数式风格进行编程。不过,它确实特别适合函数式风格,因为函数是一等对象 ,这意味着它们可以分配给变量。这也意味着函数可以作为参数传递给其他函数(通常称为回调),也可以作为其他函数的返回值。返回其他函数或接受它们作为参数的函数称为高阶函数,它们是函数式编程的基本部分。
近年来,以函数式风格编写 JavaScript 变得越来越流行,尤其是随着 React 的兴起。React 使用适合函数式方法的声明式 API,因此对函数式编程原理的深入理解将改进您的 React 代码。
为什么函数式编程这么好?
简而言之,函数式编程语言通常会生成简洁、清晰和优雅的代码。代码通常更容易测试,并且可以在多线程环境中应用而不会出现任何问题。
如果您与许多不同的程序员交谈,您可能会得到每个人对函数式编程完全不同的看法 - 从那些绝对讨厌它的人到那些绝对喜欢它的人。我们(本文的作者)处于"喜欢它"的一端,但我们完全理解这并不是每个人都喜欢的,特别是因为它与通常的编程教学方式非常不同。
然而,一旦你掌握了函数式编程的窍门,并且一旦思维过程顺利,它就会成为你的第二天性,并改变你编写代码的方式。
规则 1:净化你的功能
函数式编程的一个关键部分是确保您编写的函数是"纯粹的"。如果您不熟悉这个术语,那么纯函数本质上满足以下条件:
- 它具有引用透明度 。这意味着,给定相同的参数,该函数将始终返回相同的值。任何函数调用都可以替换为返回值,并且程序仍将以相同的方式运行。
- 它没有副作用。这意味着该函数不会在函数范围之外进行任何更改。这可以包括更改全局值、登录到控制台或更新 DOM。
纯函数必须 至少有一个参数并且必须返回一个值。如果你想一想,如果他们不接受任何参数,他们就不会有任何数据可以使用,如果他们不返回值,那么该函数的意义何在?
纯函数一开始可能并不是完全必要的,但不纯函数可能会导致程序发生整体变化,从而导致一些严重的逻辑错误!
例如:
ini
//impure
let minimum = 21
const checkAge = age => age >= minimum
//pure
const checkAge = age => {
const minimum = 21
return age >= minimum
}
在不纯函数中,checkAge
函数依赖于可变变量minimum
。例如,如果minimum
稍后要在程序中更新变量,则该checkAge
函数可能会返回具有相同输入的布尔值。
想象一下如果我们运行这个:
scss
checkAge(20) >> false
现在,让我们想象一下,在代码的后面,一个changeToUK()
函数将 的值更新minimum
为 18。
然后,想象我们运行这个:
scss
checkAge(20) >> true
checkAge
现在,尽管给出了相同的输入,但该函数仍计算出不同的值。
纯函数使您的代码更具可移植性,因为它们不依赖于作为参数提供的值之外的任何其他值。返回值永远不会改变的事实使得纯函数更容易测试。
一致地编写纯函数还消除了发生突变和副作用的可能性。
突变是函数式编程中的一个大危险信号,如果您想了解更多原因,可以在 JavaScript 中的变量赋值和突变指南 中阅读相关内容。
为了使你的函数更加可移植,请确保你的函数始终保持纯粹。
规则 2:保持变量不变
声明变量是任何程序员首先要学习的事情之一。它变得微不足道,但在使用函数式编程风格时却非常重要。
函数式编程的关键原则之一是,一旦设置了变量,它就会在整个程序中保持该状态。
这是显示代码中变量的重新分配/重新声明如何可能是一场灾难的最简单示例:
ini
const n = 10
n = 11
TypeError: "Attempted to assign to readonly property."
如果你仔细想想, 的值n
不能同时是10
和11
;这没有逻辑意义。
命令式编程中常见的编码实践是使用以下代码递增值:
ini
let x = 5
x = x + 1
在数学中,这种说法x = x + 1
是不合逻辑的,因为如果x
两边都减去,就会得到0 = 1
,这显然是不正确的。
因此,在 Haskell 中,您无法将变量分配给一个值,然后将其重新分配给另一个值。**要在 JavaScript 中实现此目的,您应该遵循始终使用 声明变量的const
**规则。
规则 3:使用箭头函数
在数学中,函数的概念是将一组值映射到另一组值的概念。下图显示了通过平方将左侧值集映射到右侧值集的函数:
这就是它在数学中用箭头表示法的写法:f: x → x²
。这意味着该函数f
将值映射x
到x²
。
我们可以使用箭头函数来几乎相同地编写这个函数:
ini
const f = x => x**2
在 JavaScript 中使用函数式风格的一个关键特征是使用箭头函数而不是常规函数。当然,这确实归结为风格,并且在常规函数上使用箭头函数实际上并不影响代码的"功能"程度。
然而,使用函数式编程风格时最难适应的事情之一是每个函数都是输入到输出的映射的思维方式。没有程序之类的东西。我们发现使用箭头函数可以帮助我们更好地理解函数的过程。
箭头函数具有隐式返回值,这确实有助于可视化此映射。
箭头函数的结构------尤其是它们的隐式返回值------有助于鼓励纯函数的编写,因为它们的字面结构是"输入映射到输出":
ini
args => returnValue
我们想要强调的另一件事,尤其是在编写箭头函数时,是三元运算符的使用。如果您不熟悉三元运算符 ,它们是内联if...else
语句,其形式为condition ? value if true : value if false
.
您可以在快速提示:如何在 JavaScript 中使用三元运算符 中阅读有关它们的更多信息。
在函数式编程中使用三元运算符的主要原因之一是语句的必要性else
。如果原始条件不满足,程序必须知道该怎么做。 例如,Haskell 强制执行一条else
语句,如果未给出语句,则会返回错误。
使用三元运算符的另一个原因是它们是始终返回值的表达式 ,而不是if-else
可用于执行具有潜在副作用的操作的语句。这对于箭头函数特别有用,因为这意味着您可以确保有返回值并保留将输入映射到输出的图像。如果您不确定语句和表达式之间的细微差别,那么这份关于语句与表达式的指南非常值得一读。
为了说明这两个条件,下面是一个使用三元运算符的简单箭头函数的示例:
ini
const action = state => state === "hungry" ? "eat cake" : "sleep"
该action
函数将根据参数的值返回"eat"或"sleep"值state
。
因此,总结一下:当让你的代码更具功能性时,你应该遵循以下两条规则:
- 使用箭头符号编写函数
if...else
用三元运算符替换语句
规则 4:删除 For 循环
鉴于使用for
循环来编写迭代代码在编程中非常常见,因此说要避免它们似乎很奇怪。事实上,当我们第一次发现 Haskell 甚至没有任何类型的for
循环操作时,我们很难理解如何实现一些标准操作。然而,有一些很好的理由可以解释为什么for
循环不会出现在函数式编程中,并且我们很快发现每种类型的迭代过程都可以在不使用for
循环的情况下实现。
不使用循环的最重要原因for
是它们依赖于可变状态。让我们看一个简单的sum
函数:
ini
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 ,并且对于我们在循环内更新的变量也必须使用 a for
。
正如已经解释的那样,这在函数式编程中通常是不好的做法,因为函数式编程中的所有变量都应该是不可变的。
如果我们想编写所有变量都是不可变的代码,我们可以使用递归:
bash
const sum = n => n === 1 ? 1 : n + sum(n-1)
正如您所看到的,没有变量被更新。
我们当中的数学家显然会知道所有这些代码都是不必要的,因为我们可以使用 的漂亮求和公式0.5*n*(n+1)
。但这是说明循环可变性与递归之间差异的好方法for
。
不过,递归并不是解决可变性问题的唯一解决方案,特别是当我们处理数组时。JavaScript 有许多内置的高阶数组方法,这些方法可以循环数组中的值,而不会改变任何变量。
例如,假设我们想为数组中的每个值加 1。使用命令式方法和for
循环,我们的函数可能如下所示:
php
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
方法来代替循环,并编写一个如下所示的函数:
c
const addOne = array => array.map(x => x + 1)
如果您以前从未见过某个map
函数,那么绝对值得学习它们 - 以及 JavaScript 的所有内置高阶数组方法,例如filter
,特别是如果您真的对 JavaScript 中的函数式编程感兴趣的话。您可以在不可变数组方法:如何编写非常干净的 JavaScript 代码中找到有关它们的更多信息。
for
Haskell根本没有循环。为了使 JavaScript 更加实用,请尝试通过使用递归和内置的高阶数组方法来避免使用 for 循环。
规则 5:避免类型强制
当使用 JavaScript 等不需要类型声明的语言进行编程时,很容易忘记数据类型的重要性。JavaScript 中使用的七种基本数据类型是:
- 数字
- 细绳
- 布尔值
- 象征
- 大整型
- 不明确的
- 无效的
Haskell 是一种强类型 语言,需要类型声明。这意味着,在执行任何函数之前,您需要使用Hindley-Milner 系统指定输入数据的类型和输出数据的类型。
例如:
sql
add :: Integer -> Integer -> Integer
add x y = x + y
这是一个非常简单的函数,将两个数字相加(x
和y
)。必须向程序解释每个函数的数据类型是什么,包括像这样的非常简单的函数,这似乎有点荒谬,但最终它有助于显示函数的工作原理以及预期返回的内容。这使得代码更容易调试,尤其是当代码开始变得更加复杂时。
类型声明遵循以下结构:
css
functionName :: inputType(s) -> outputType
使用 JavaScript 时,类型强制可能是一个大问题,因为 JavaScript 有各种可以用来(甚至滥用)来解决数据类型不一致问题的技巧。以下是最常见的问题以及如何避免它们:
- 串联 。
"Hello" + 5
计算结果为"Hello5"
,这不一致。如果你想连接一个字符串和一个数值,你应该写"Hello" + String(5)
. - 布尔语句和 0 。
0
在 JavaScript 中,语句中的值if
相当于false
. 这可能会导致惰性编程技术,忽略检查数值数据是否等于0
。
例如:
ini
const even = n => !(n%2)
这是一个评估数字是否为偶数的函数。它使用!
符号将 的结果强制转换n%2 ?
为布尔值,但 的结果n%2
不是布尔值,而是数字( 或0
)1
。
像这样的黑客虽然看起来很聪明并减少了编写的代码量,但却打破了函数式编程的类型一致性规则。因此,编写此函数的最佳方法如下:
dart
// even :: Number -> Number
const even = n => n%2 === 0
另一个重要的概念是确保数组中的所有数据值都具有相同的类型。这不是由 JavaScript 强制执行的,但当您想要使用高阶数组方法时,不具有相同的类型可能会导致问题。
例如,product
将数组中的所有数字相乘并返回结果的函数可以使用以下类型声明注释编写:
javascript
// product :: [ Number ] -> Number
const product = numbers => numbers.reduce((s,x) => x * s,1)
在这里,类型声明清楚地表明函数的输入是一个包含 类型元素的数组Number
,但它仅返回一个数字。类型声明清楚地表明了该函数的输入和输出的预期内容。显然,如果数组不只包含数字,则该函数将无法工作。
Haskell 是一种强类型语言,而 JavaScript 是弱类型语言,但为了使 JavaScript 更具功能性,您应该在声明函数之前编写类型声明注释,并确保避免类型强制快捷方式。
我们还应该在这里提到,如果您想要 JavaScript 的强类型替代品来强制类型一致性,那么您显然可以转向TypeScript 。
结论
总而言之,以下五个规则将帮助您实现功能性代码:
- 保持你的函数纯粹。
- 始终使用const声明变量和函数。
- 对函数使用箭头表示法。
- 避免使用
for
循环。 - 使用类型声明注释并避免类型强制快捷方式。
虽然这些规则不能保证您的代码纯粹是功能性的,但它们将在使其更具功能性并帮助使其更加简洁、清晰和更易于测试方面大有帮助。
我们真诚地希望这些规则能够像帮助我们一样帮助您!我们都是函数式编程的忠实粉丝,我们强烈鼓励任何程序员使用它。