了解使用JavaScript进行函数式编程

了解使用JavaScript进行函数式编程

经过长时间的学习和使用面向对象的编程,我退后了一步来思考系统的复杂性。

“Complexity is anything that makes software hard to understand or to modify. “ —约翰·奥特豪特

经过研究,我发现了函数式编程概念,例如不变性和纯函数。 这些概念是构建无副作用功能的巨大优势,因此,维护系统更容易-还有其他好处

在本文中,我将通过许多代码示例向您详细介绍函数式编程和一些重要概念。 用Javascript!

什么是函数式编程?

函数式编程是一种编程范式-一种构建计算机程序的结构和元素的方式-将计算视为对数学函数的评估,并且避免了状态和可变数据的更改- *

纯功能

了解使用JavaScript进行函数式编程
Mohan Murugesan在《 Unsplash 》上的“水滴”

当我们想了解函数式编程时,我们学习的第一个基本概念是纯函数 但这到底是什么意思? 是什么使函数纯净?

那么我们如何知道一个函数是否pure呢? 这是一个非常严格的纯度定义:

  • 如果给定相同的参数,它将返回相同的结果(也称为deterministic
  • 它不会引起任何明显的副作用

如果给定相同的参数,它将返回相同的结果

假设我们要实现一个计算圆的面积的函数。 一个不纯函数将接收radius作为参数,然后计算radius * radius * PI

为什么这是不纯功能? 仅仅是因为它使用了一个没有作为参数传递给函数的全局对象。

现在想象一些数学家认为PI值实际上是42并且会更改全局对象的值。

我们的不纯函数现在将导致10 * 10 * 42 = 4200 对于相同的参数( radius = 10 ),我们得到不同的结果。 让我们修复它!

TA-DA????! 现在,我们将始终将PI值作为参数传递给函数。 因此,现在我们只访问传递给函数的参数。 没有external object

  • 对于参数radius = 10PI = 3.14 ,我们将始终具有相同的结果: 314.0
  • 对于参数radius = 10PI = 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进行函数式编程
罗斯· 芬顿Ross Findon)在《 Unsplash 》上的“更改霓虹灯标牌”

当数据不可变时,其状态无法更改 创建之后。 如果要更改不可变对象,则不能。 而是使用新值创建一个新对象。

在Javascript中,我们通常使用for循环。 接下来的for语句具有一些可变变量。

对于每次迭代,我们都会更改isumOfValue 状态 但是,我们如何处理迭代中的可变性? 递归!

因此,这里有sum函数,用于接收数值向量。 该函数将自行调用,直到列表为空( 递归 base case )为止。 对于每个“迭代”,我们会将其值添加到total累加器中。

通过递归,我们保留变量 一成不变的。 listaccumulator变量不变。 它保持相同的值。

观察 :是的! 我们可以使用reduce来实现此功能。 我们将在“ Higher Order Functions主题中对此进行介绍。

建立对象的最终状态也很常见。 假设我们有一个字符串,并且我们想将此字符串转换为url slug

在Ruby的OOP中,我们将创建一个类,例如UrlSlugify 这节课会有一个slugify! 将字符串输入转换为url slug

美丽! 它实现了! 在这里,我们必须进行命令式编程,确切地说出每个slugify处理过程中要执行的操作-首先是小写字母,然后删除无用的空格,最后用连字符替换其余的空格。

但是我们在这个过程中正在改变输入状态。

我们可以通过执行功能组合或功能链接来处理此突变。 换句话说,函数的结果将用作下一个函数的输入,而无需修改原始输入字符串。

这里我们有:

  • toLowerCase :将字符串转换为所有小写
  • trim :删除字符串两端的空格
  • split and join :用给定字符串中的替换替换所有match实例

我们将所有这四个函数结合在一起,就可以"slugify"字符串了。

参照透明

了解使用JavaScript进行函数式编程
乔什·卡拉布雷斯Josh Calabrese)在“ Unsplash ”上发表的“戴眼镜的人”

让我们实现一个square function

给定相同的输入,此纯函数将始终具有相同的输出。

传递2作为square function的参数将始终返回4。因此,现在我们可以将square(2)替换为4。就是这样! 我们的功能是referentially transparent

基本上,如果一个函数对于相同的输入始终产生相同的结果,则它是参照透明的。

纯函数+不可变数据=参考透明

有了这个概念,我们可以做的一件很酷的事情就是记住该功能。 想象一下我们具有以下功能:

我们用以下参数调用它:

sum(5, 8)等于13 此功能将始终导致13 因此,我们可以这样做:

这个表达式将始终为16 我们可以将整个表达式替换为数值常量并进行记忆

作为一流实体

了解使用JavaScript进行函数式编程
安德鲁·尼尔Under Splash)的 “一流”

函数作为一等实体的想法是将函数视为值用作数据。

作为一流实体的功能可以:

  • 从常量和变量中引用它
  • 将其作为参数传递给其他函数
  • 作为其他函数的结果返回

想法是将函数视为值,并像数据一样传递函数。 这样,我们可以组合不同的功能来创建具有新行为的新功能。

假设我们有一个将两个值相加然后将值加倍的函数。 像这样:

现在,一个将值相减并返回双精度值的函数:

这些功能具有相似的逻辑,但是区别在于运算符功能。 如果我们可以将函数视为值并将其作为参数传递,则可以构建一个接收操作符函数并在函数内部使用的函数。 让我们来构建它!

做完了! 现在我们有一个f参数,并用它来处理ab 我们传递了sumsubtraction函数来与doubleOperator函数组合并创建新行为。

高阶函数

当我们谈论高阶函数时,我们指的是以下函数之一:

  • 将一个或多个函数作为参数,或
  • 返回一个函数作为其结果

我们上面实现的doubleOperator函数是一个高阶函数,因为它将运算符作为参数并使用它。

您可能已经听说过filtermapreduce 让我们来看看这些。

过滤

给定一个集合,我们想按属性过滤。 筛选器函数期望使用truefalse值来确定是否应将元素包含在结果集合中。 基本上,如果回调表达式为true ,则过滤器函数会将元素包括在结果集合中。 否则,它将不会。

一个简单的例子是当我们有一个整数集合并且我们只需要偶数时。

势在必行

使用Javascript的一种必要方法是:

  • 创建一个空数组evenNumbers
  • 遍历numbers数组
  • 将偶数推送到evenNumbers数组

我们还可以使用filter高阶函数来接收even函数,并返回偶数列表:

我在Hacker Rank FP路径上解决的一个有趣的问题是“ 过滤器阵列”问题 问题的思想是过滤给定的整数数组,仅输出小于指定值X那些值。

解决此问题的强制性Javascript解决方案如下:

我们确切地说出函数需要做的事情–遍历集合,将集合当前项与x进行比较,如果该元素通过条件,则将其推送到resultArray

声明式方法

但是,我们需要一种更具声明性的方式来解决此问题,并同时使用filter高阶函数。

声明式Javascript解决方案如下所示:

首先,在smaller函数中使用this功能似乎有些奇怪,但很容易理解。

this将是filter功能中的第二个参数。 在这种情况下, 3x )表示为this 而已。

我们也可以使用地图来做到这一点。 想象一下,我们有一幅nameage的地图。

并且我们只希望过滤特定年龄段的人员,在此示例中,年龄超过21岁的人员。

代码摘要:

  • 我们有一个人的名单( nameage )。
  • 我们有一个功能olderThan21 在这种情况下,对于人员阵列中的每个人,我们要访问age并查看age是否大于21岁。
  • 我们基于此功能过滤所有人员。

地图

map的想法是转换集合。

map方法通过将函数应用于其所有元素并根据返回的值构建新集合来转换集合。

让我们得到上面的同一people集合。 我们现在不想按“年龄超过”进行过滤。 我们只想要一个字符串列表,例如TK is 26 years old 因此,最后一个字符串可能是:name is :age years old ,其中:name:agepeople集合中每个元素的属性。

以命令式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 1Product 2Product 3Product 4到购物车(订单)。 现在,我们要计算购物车的总金额。

以必要的方式,我们将迭代订单清单,并将每个产品的数量加到总数量上。

使用reduce ,我们可以构建一个函数来处理amount sum并将其作为参数传递给reduce函数。

在这里,我们有shoppingCart ,接收当前currentTotalAmount的功能sumAmount以及用于对它们sumorder对象。

getTotalAmount函数用于通过使用sumAmount并从0开始reduce shoppingCart sumAmount

获得总量的另一种方法是组成mapreduce 那是什么意思 我们可以使用mapshoppingCart转换为amount值的集合,然后仅将reduce函数与sumAmount函数一起使用。

getAmount接收产品对象,并仅返回amount值。 所以我们这里是[10, 30, 20, 60] 然后reduce将所有项目相加在一起。 美丽!

我们看了每个高阶函数的工作原理。 我想向您展示一个示例,说明如何在一个简单示例中组合所有这三个函数。

谈论shopping cart ,想象一下我们的订单中有以下产品清单:

我们想要购物车中所有书籍的总数。 就那么简单。 算法?

  • 按书本类型过滤
  • 使用地图将购物车转化为金额集合
  • 通过将所有项目与reduce相加来合并

做完了! ????

资源资源

我整理了一些阅读和学习的资源。 我正在分享我发现非常有趣的内容。 有关更多资源,请访问我的Functional Programming Github存储库

简介

纯功能

不变的数据

高阶函数

声明式编程

而已!

大家好,我希望您在阅读这篇文章时玩得开心,也希望您在这里学到了很多东西! 这是我分享我所学内容的尝试。

这是本文中所有代码的存储库

来跟我学习。 我正在这个“ 学习功能编程”存储库中共享资源和代码。

我还写了一篇FP帖子,但主要使用 Clojure❤。

希望您在这里看到了对您有用的东西。 下次见! :)

我的TwitterGithub

TK。

From: https://hackernoon.com/understanding-functional-programming-with-javascript-41eb3fa8c2a