了解使用JavaScript进行函数式编程
经过长时间的学习和使用面向对象的编程,我退后了一步来思考系统的复杂性。
“Complexity is anything that makes software hard to understand or to modify.
“ —约翰·奥特豪特
经过研究,我发现了函数式编程概念,例如不变性和纯函数。 这些概念是构建无副作用功能的巨大优势,因此,维护系统更容易-还有其他好处 。
在本文中,我将通过许多代码示例向您详细介绍函数式编程和一些重要概念。 用Javascript!
什么是函数式编程?
函数式编程是一种编程范式-一种构建计算机程序的结构和元素的方式-将计算视为对数学函数的评估,并且避免了状态和可变数据的更改- 维基百科
纯功能
当我们想了解函数式编程时,我们学习的第一个基本概念是纯函数 。 但这到底是什么意思? 是什么使函数纯净?
那么我们如何知道一个函数是否pure
呢? 这是一个非常严格的纯度定义:
- 如果给定相同的参数,它将返回相同的结果(也称为
deterministic
) - 它不会引起任何明显的副作用
如果给定相同的参数,它将返回相同的结果
假设我们要实现一个计算圆的面积的函数。 一个不纯函数将接收radius
作为参数,然后计算radius * radius * PI
:
为什么这是不纯功能? 仅仅是因为它使用了一个没有作为参数传递给函数的全局对象。
现在想象一些数学家认为PI
值实际上是42
并且会更改全局对象的值。
我们的不纯函数现在将导致10 * 10 * 42
= 4200
。 对于相同的参数( radius = 10
),我们得到不同的结果。 让我们修复它!
TA-DA????! 现在,我们将始终将PI
值作为参数传递给函数。 因此,现在我们只访问传递给函数的参数。 没有external object
。
- 对于参数
radius = 10
&PI = 3.14
,我们将始终具有相同的结果:314.0
- 对于参数
radius = 10
&PI = 42
,我们将始终具有相同的结果:4200
读取文件
如果我们的函数读取外部文件,则它不是纯粹的函数-文件的内容可以更改。
随机数生成
任何依赖随机数生成器的函数都不能是纯函数。
它不会引起任何明显的副作用
可观察到的副作用的示例包括修改全局对象或通过引用传递的参数。
现在我们要实现一个函数,以接收一个整数值并返回增加了1的值。
我们有对counter
。 我们的不纯函数接收该值,然后将值增加1的计数器重新分配。
观察 :在函数式编程中不鼓励可变性。
我们正在修改全局对象。 但是,我们将如何使其pure
呢? 只需返回增加1的值即可。就这么简单。
看到我们的纯函数increaseCounter
返回2,但是counter
值仍然相同。 该函数将返回递增的值,而不更改变量的值。
如果我们遵循这两个简单的规则,就会更容易理解我们的程序。 现在,每个功能都是孤立的,无法影响我们系统的其他部分。
纯函数是稳定,一致和可预测的。 给定相同的参数,纯函数将始终返回相同的结果。 我们不需要考虑相同参数产生不同结果的情况-因为它永远不会发生。
纯功能的好处
该代码绝对更容易测试。 我们不需要嘲笑任何东西。 因此,我们可以对具有不同上下文的纯函数进行单元测试:
- 给定参数
A
→期望函数返回值B
- 给定参数
C
→期望函数返回值D
一个简单的示例是一个函数,该函数接收一个数字集合,并期望它增加该集合的每个元素。
我们收到了numbers
数组,使用map
递增每个数字,并返回一个新的递增数字列表。
对于input
[1, 2, 3, 4, 5]
,预期output
为[2, 3, 4, 5, 6]
。
不变性
随着时间的推移不变或无法更改。
当数据不可变时,其状态无法更改 创建之后。 如果要更改不可变对象,则不能。 而是使用新值创建一个新对象。
在Javascript中,我们通常使用for
循环。 接下来的for
语句具有一些可变变量。
对于每次迭代,我们都会更改i
和sumOfValue
状态 。 但是,我们如何处理迭代中的可变性? 递归!
因此,这里有sum
函数,用于接收数值向量。 该函数将自行调用,直到列表为空( 递归 base case
)为止。 对于每个“迭代”,我们会将其值添加到total
累加器中。
通过递归,我们保留变量 一成不变的。 list
和accumulator
变量不变。 它保持相同的值。
观察 :是的! 我们可以使用reduce
来实现此功能。 我们将在“ Higher Order Functions
主题中对此进行介绍。
建立对象的最终状态也很常见。 假设我们有一个字符串,并且我们想将此字符串转换为url slug
。
在Ruby的OOP中,我们将创建一个类,例如UrlSlugify
。 这节课会有一个slugify!
将字符串输入转换为url slug
。
美丽! 它实现了! 在这里,我们必须进行命令式编程,确切地说出每个slugify
处理过程中要执行的操作-首先是小写字母,然后删除无用的空格,最后用连字符替换其余的空格。
但是我们在这个过程中正在改变输入状态。
我们可以通过执行功能组合或功能链接来处理此突变。 换句话说,函数的结果将用作下一个函数的输入,而无需修改原始输入字符串。
这里我们有:
-
toLowerCase
:将字符串转换为所有小写 -
trim
:删除字符串两端的空格 -
split
andjoin
:用给定字符串中的替换替换所有match实例
我们将所有这四个函数结合在一起,就可以"slugify"
字符串了。
参照透明
让我们实现一个square function
:
给定相同的输入,此纯函数将始终具有相同的输出。
传递2
作为square function
的参数将始终返回4。因此,现在我们可以将square(2)
替换为4。就是这样! 我们的功能是referentially transparent
。
基本上,如果一个函数对于相同的输入始终产生相同的结果,则它是参照透明的。
纯函数+不可变数据=参考透明
有了这个概念,我们可以做的一件很酷的事情就是记住该功能。 想象一下我们具有以下功能:
我们用以下参数调用它:
sum(5, 8)
等于13
。 此功能将始终导致13
。 因此,我们可以这样做:
这个表达式将始终为16
。 我们可以将整个表达式替换为数值常量并进行记忆 。
作为一流实体
函数作为一等实体的想法是将函数也视为值并用作数据。
作为一流实体的功能可以:
- 从常量和变量中引用它
- 将其作为参数传递给其他函数
- 作为其他函数的结果返回
想法是将函数视为值,并像数据一样传递函数。 这样,我们可以组合不同的功能来创建具有新行为的新功能。
假设我们有一个将两个值相加然后将值加倍的函数。 像这样:
现在,一个将值相减并返回双精度值的函数:
这些功能具有相似的逻辑,但是区别在于运算符功能。 如果我们可以将函数视为值并将其作为参数传递,则可以构建一个接收操作符函数并在函数内部使用的函数。 让我们来构建它!
做完了! 现在我们有一个f
参数,并用它来处理a
和b
。 我们传递了sum
和subtraction
函数来与doubleOperator
函数组合并创建新行为。
高阶函数
当我们谈论高阶函数时,我们指的是以下函数之一:
- 将一个或多个函数作为参数,或
- 返回一个函数作为其结果
我们上面实现的doubleOperator
函数是一个高阶函数,因为它将运算符作为参数并使用它。
您可能已经听说过filter
, map
和reduce
。 让我们来看看这些。
过滤
给定一个集合,我们想按属性过滤。 筛选器函数期望使用true
或false
值来确定是否应将元素包含在结果集合中。 基本上,如果回调表达式为true
,则过滤器函数会将元素包括在结果集合中。 否则,它将不会。
一个简单的例子是当我们有一个整数集合并且我们只需要偶数时。
势在必行
使用Javascript的一种必要方法是:
- 创建一个空数组
evenNumbers
- 遍历
numbers
数组 - 将偶数推送到
evenNumbers
数组
我们还可以使用filter
高阶函数来接收even
函数,并返回偶数列表:
我在Hacker Rank FP路径上解决的一个有趣的问题是“ 过滤器阵列”问题 。 问题的思想是过滤给定的整数数组,仅输出小于指定值X
那些值。
解决此问题的强制性Javascript解决方案如下:
我们确切地说出函数需要做的事情–遍历集合,将集合当前项与x
进行比较,如果该元素通过条件,则将其推送到resultArray
。
声明式方法
但是,我们需要一种更具声明性的方式来解决此问题,并同时使用filter
高阶函数。
声明式Javascript解决方案如下所示:
首先,在smaller
函数中使用this
功能似乎有些奇怪,但很容易理解。
this
将是filter
功能中的第二个参数。 在这种情况下, 3
( x
)表示为this
。 而已。
我们也可以使用地图来做到这一点。 想象一下,我们有一幅name
和age
的地图。
并且我们只希望过滤特定年龄段的人员,在此示例中,年龄超过21岁的人员。
代码摘要:
- 我们有一个人的名单(
name
和age
)。 - 我们有一个功能
olderThan21
。 在这种情况下,对于人员阵列中的每个人,我们要访问age
并查看age
是否大于21岁。 - 我们基于此功能过滤所有人员。
地图
map的想法是转换集合。
map
方法通过将函数应用于其所有元素并根据返回的值构建新集合来转换集合。
让我们得到上面的同一people
集合。 我们现在不想按“年龄超过”进行过滤。 我们只想要一个字符串列表,例如TK is 26 years old
。 因此,最后一个字符串可能是:name is :age years old
,其中:name
和:age
是people
集合中每个元素的属性。
以命令式Javascript方式,它将是:
用声明性Javascript方式,它将是:
整个想法是将给定的数组转换为新的数组。
另一个有趣的Hacker Rank问题是更新列表问题 。 我们只想用其绝对值更新给定数组的值。
例如,输入[1, 2, 3, -4, 5]
需要将输出为[1, 2, 3, 4, 5]
。 -4
的绝对值为4
。
一个简单的解决方案是就每个集合值进行就地更新。
我们使用Math.abs
函数将值转换为其绝对值,并进行就地更新。
这不是实现此解决方案的功能方法。
首先,我们了解了不变性。 我们知道不变性对于使我们的功能更加一致和可预测非常重要。 这个想法是建立一个具有所有绝对值的新集合。
其次,为什么不使用map
来“转换”所有数据?
我的第一个想法是测试Math.abs
函数以仅处理一个值。
我们希望将每个值转换为正值(绝对值)。
现在我们知道如何对一个值进行absolute
运算,我们可以使用此函数作为参数传递给map
函数。 您还记得higher order function
可以将函数作为参数来使用吗? 是的,地图可以做到!
哇。 如此美丽! ????
降低
reduce的想法是接收一个函数和一个集合,并返回通过组合项目创建的值。
人们谈论的一个常见示例是获取订单的总金额。 想象一下您在一个购物网站上。 您已将Product 1
, Product 2
, Product 3
和Product 4
到购物车(订单)。 现在,我们要计算购物车的总金额。
以必要的方式,我们将迭代订单清单,并将每个产品的数量加到总数量上。
使用reduce
,我们可以构建一个函数来处理amount sum
并将其作为参数传递给reduce
函数。
在这里,我们有shoppingCart
,接收当前currentTotalAmount
的功能sumAmount
以及用于对它们sum
的order
对象。
getTotalAmount
函数用于通过使用sumAmount
并从0
开始reduce
shoppingCart
sumAmount
。
获得总量的另一种方法是组成map
和reduce
。 那是什么意思 我们可以使用map
将shoppingCart
转换为amount
值的集合,然后仅将reduce
函数与sumAmount
函数一起使用。
getAmount
接收产品对象,并仅返回amount
值。 所以我们这里是[10, 30, 20, 60]
。 然后reduce
将所有项目相加在一起。 美丽!
我们看了每个高阶函数的工作原理。 我想向您展示一个示例,说明如何在一个简单示例中组合所有这三个函数。
谈论shopping cart
,想象一下我们的订单中有以下产品清单:
我们想要购物车中所有书籍的总数。 就那么简单。 算法?
- 按书本类型过滤
- 使用地图将购物车转化为金额集合
- 通过将所有项目与reduce相加来合并
做完了! ????
资源资源
我整理了一些阅读和学习的资源。 我正在分享我发现非常有趣的内容。 有关更多资源,请访问我的Functional Programming Github存储库 。
简介
纯功能
不变的数据
高阶函数
声明式编程
而已!
大家好,我希望您在阅读这篇文章时玩得开心,也希望您在这里学到了很多东西! 这是我分享我所学内容的尝试。
来跟我学习。 我正在这个“ 学习功能编程”存储库中共享资源和代码。
我还写了一篇FP帖子,但主要使用 Clojure❤。
希望您在这里看到了对您有用的东西。 下次见! :)
TK。
From: https://hackernoon.com/understanding-functional-programming-with-javascript-41eb3fa8c2a