JavaScript中的函数式编程原理

该材料的作者(我们今天出版的翻译版)说,经过长时间的面向对象编程,他思考了系统的复杂性。 根据John Ousterhout所说,复杂性使理解或修改软件变得更加困难。 本文作者完成了一些研究,发现了免疫和纯函数之类的函数编程概念。 通过使用此类概念,您可以创建没有副作用的功能。 使用这些功能可简化系统支持,并为程序员提供其他好处

图片

在这里,我们讨论函数式编程及其一些重要原理。 所有这些将通过许多JavaScript代码示例进行说明。

什么是函数式编程?


您可以在Wikipedia上了解什么是函数式编程。 即,我们正在谈论以下事实:函数式编程是一种编程范例,其中在对后者的数学理解中,将计算过程视为计算函数的值。 函数式编程涉及根据源数据和其他函数的结果来计算函数的结果,并不意味着显式存储程序状态。 因此,这并不意味着该状态的可变性。

现在,通过示例,我们将分析一些函数式编程的思想。

纯功能


为了理解函数式编程的本质,纯函数是需要研究的第一个基本概念。

什么是“纯功能”? 是什么使功能“干净”? 纯函数必须满足以下要求:

  • 当将相同的参数传递给它时,它总是返回相同的结果(此类函数也称为确定性)。
  • 这样的功能没有副作用。

考虑纯函数的第一个属性,即当将相同的参数传递给它们时,它们始终返回相同的结果这一事实。

▍函数参数和返回值


想象一下,我们需要创建一个计算圆的面积的函数。 非纯函数将以圆的半径( radius )作为参数,此后它将返回表达式radius * radius * PI的计算值:

 const PI = 3.14; function calculateArea(radius) { return radius * radius * PI; } calculateArea(10); //  314 

为什么不能将此函数称为纯函数? 事实是它使用了一个全局常量,该常量不会作为参数传递给它。

现在想象一些数学家得出的结论是,常数PI的值应为数字42 ,因此该常数的值发生了变化。

现在,一个不纯的函数在传递相同的输入值10时将返回值10 * 10 * 42 = 4200 。 事实证明,在这里使用与上一个示例相同的radius参数值,该函数将返回不同的结果。 让我们解决这个问题:

 const PI = 3.14; function calculateArea(radius, pi) { return radius * radius * pi; } calculateArea(10, PI); //  314 

现在,当调用此函数时,我们将始终将参数pi传递pi 。 结果,该函数将仅与调用时传递给它的函数一起使用,而无需求助于全局实体。 如果我们分析此功能的行为,我们可以得出以下结论:

  • 如果函数传递的参数radius等于10 ,参数pi等于3.14 ,它将始终返回相同的结果314
  • radius参数为10pi42调用时,它将始终返回4200

读取文件


如果我们的函数读取文件,那么它将不干净。 事实是文件的内容可能会更改。

 function charactersCounter(text) { return `Character count: ${text.length}`; } function analyzeFile(filename) { let fileContent = open(filename); return charactersCounter(fileContent); } 

随机数生成


任何依赖随机数生成器的函数都不能是纯函数。

 function yearEndEvaluation() { if (Math.random() > 0.5) {   return "You get a raise!"; } else {   return "Better luck next year!"; } } 

现在让我们谈谈副作用。

▍副作用


调用函数时可能发生的副作用的一个例子是对通过引用传递给函数的全局变量或参数的修改。

假设我们需要创建一个函数,该函数需要一个整数并将该数字加1。这是类似想法的实现的样子:

 let counter = 1; function increaseCounter(value) { counter = value + 1; } increaseCounter(counter); console.log(counter); // 2 

有一个全局变量counter 。 我们的函数(不是纯函数)会将该值作为参数接收并覆盖它,并将其值添加到其前一个值。

全局变量正在变化,不欢迎在函数编程中进行类似操作。

在我们的例子中,全局变量的值被修改。 在这种情况下如何使increaseCounter()函数变得干净? 实际上,这非常简单:

 let counter = 1; function increaseCounter(value) { return value + 1; } increaseCounter(counter); // 2 console.log(counter); // 1 

如您所见,该函数返回2 ,但是全局变量counter的值不变。 在这里我们可以得出结论,该函数返回传递给它的值,该值增加了1 ,而没有任何改变。

如果您遵循上述两个编写纯函数的规则,这将使在使用此类函数创建的程序中浏览变得更加容易。 事实证明,每个功能都是独立的,不会影响程序外部的各个部分。

纯函数是稳定,一致和可预测的。 接收到相同的输入数据后,此类函数将始终返回相同的结果。 这样就避免了程序员试图考虑相同参数的函数传递导致不同结果的可能性,因为对于纯函数而言这是根本不可能的。

pure纯功能的优势


纯函数的优点之一是使用它们编写的代码更易于测试。 特别是,您不需要创建任何存根对象。 这允许在各种情况下对纯函数进行单元测试:

  • 如果将参数A传递给函数,则期望B的返回值。
  • 如果将参数C传递给函数,则D的返回值是预期的。

作为此想法的简单示例,我们可以提供一个接受数字数组的函数,并且预计该数组的每个数字将增加一个,并返回一个包含结果的新数组:

 let list = [1, 2, 3, 4, 5]; function incrementNumbers(list) { return list.map(number => number + 1); } 

在这里,我们将一个数字数组传递给该函数,然后使用map()数组方法,该方法允许我们修改该数组的每个元素并形成该函数返回的新数组。 我们通过传递一个list数组来调用该函数:

 incrementNumbers(list); //  [2, 3, 4, 5, 6] 

从这个函数可以期望,在接受了[1, 2, 3, 4, 5]形式的数组之后,它将返回一个新的数组[2, 3, 4, 5, 6] 。 这就是它的工作原理。

豁免权


某个实体的豁免权可以描述为它不会随时间变化的事实,也可以描述为无法更改该实体。

如果他们尝试更改不可变的对象,则此操作将不会成功。 相反,您将需要创建一个包含新值的新对象。

例如,JavaScript通常使用for循环。 如下所示,在他的工作过程中,使用了可变变量:

 var values = [1, 2, 3, 4, 5]; var sumOfValues = 0; for (var i = 0; i < values.length; i++) { sumOfValues += values[i]; } sumOfValues // 15 

在循环的每次迭代中,变量i的值和全局变量的值(可以认为是程序的状态) sumOfValues 。 在这种情况下如何保持实体的不变性? 答案在于使用递归。

 let list = [1, 2, 3, 4, 5]; let accumulator = 0; function sum(list, accumulator) { if (list.length == 0) {   return accumulator; } return sum(list.slice(1), accumulator + list[0]); } sum(list, accumulator); // 15 list; // [1, 2, 3, 4, 5] accumulator; // 0 

有一个功能sum() ,它接受一个数字数组。 此函数将自行调用,直到数组为空为止(这是递归算法的基本情况)。 在每个这样的“迭代”中,我们将数组元素之一的值添加到accumulator函数的参数中,而不会影响全局变量accumulator 。 在这种情况下,全局变量listaccumulator保持不变;在函数调用之前和之后,相同的值存储在它们中。

应该注意的是,要实现这种算法,可以使用reduce array方法。 我们将在下面讨论。

在编程中,当有必要基于对象的某个模板来创建其最终表示形式时,该任务就变得很普遍。 想象一下,我们有一个字符串,需要将其转换为适合用作导致某种资源的URL的一部分的视图。

如果我们使用Ruby并使用OOP原理解决了这个问题,我们将首先创建一个类,称为UrlSlugify ,然后为该类slugify!创建一个方法slugify! 用于转换字符串。

 class UrlSlugify attr_reader :text def initialize(text)   @text = text end def slugify!   text.downcase!   text.strip!   text.gsub!(' ', '-') end end UrlSlugify.new(' I will be a url slug   ').slugify! # "i-will-be-a-url-slug" 

我们已经实现了算法,这很棒。 在这里,我们看到了一种势在必行的编程方法,即在处理线,绘制其转换的每个步骤时。 即,首先我们将其字符减小为小写,然后删除不必要的空格,最后更改破折号上的其余空格。

但是,在此转换过程中,会发生程序状态的突变。

您可以通过编写函数或链接函数调用来解决变异问题。 换句话说,函数返回的结果将用作下一个函数的输入,因此将用作链中所有函数的输入。 在这种情况下,原始字符串将不会更改。

 let string = " I will be a url slug   "; function slugify(string) { return string.toLowerCase()   .trim()   .split(" ")   .join("-"); } slugify(string); // i-will-be-a-url-slug 

在这里,我们使用以下函数,这些函数用标准的字符串和数组方法在JavaScript中表示:

  • toLowerCase :将字符串字符转换为toLowerCase
  • trim :从行的开头和结尾删除空格。
  • split :将字符串拆分为多个部分,将单词之间用空格隔开。
  • join :根据包含单词的数组,形成由单词组成的字符串,并用短划线分隔单词。

这四个函数使您可以创建一个用于转换字符串的函数,该函数不会更改此字符串本身。

链接透明度


创建一个函数square() ,该函数返回将数字乘以相同数字的结果:

 function square(n) { return n * n; } 

这是一个纯函数,对于相同的输入值,将始终返回相同的输出值。

 square(2); // 4 square(2); // 4 square(2); // 4 // ... 

例如,无论传递给它多少个数字2 ,此函数将始终返回数字4 。 结果证明,可以用数字4代替square(2)形式的调用。 这意味着我们的函数具有参照透明性。

通常,我们可以说,如果函数对于传递给它的相同输入值总是返回相同的结果,则它具有参照透明性。

functions纯函数+不可变数据=参照透明性


使用本节标题中提出的想法,您可以记忆功能。 假设我们有一个像这样的函数:

 function sum(a, b) { return a + b; } 

我们这样称呼它:

 sum(3, sum(5, 8)); 

调用sum(5, 8) 5,8 sum(5, 8)总是得到13 。 因此,上述调用可以重写为:

 sum(3, 13); 

反过来,此表达式始终给出16 。 结果,可以将其替换为数值常量并记录下来

用作一流对象


将函数视为第一类对象的想法是,可以将这些函数视为值,并使用它们作为数据。 可以区分以下功能:

  • 对函数的引用可以存储在常量和变量中,并可以通过它们访问函数。
  • 可以将函数作为参数传递给其他函数。
  • 可以从其他函数返回函数。

也就是说,它是关于将函数视为值并将它们像数据一样对待。 使用这种方法,可以在创建实现新功能的新功能的过程中组合各种功能。

想象一下,我们有一个函数,将传递给它的两个数值相加,然后将它们乘以2 ,然后返回结果:

 function doubleSum(a, b) { return (a + b) * 2; } 

现在,我们编写一个函数,该函数从传递给它的第一个数值中减去第二个数值,将发生的事情乘以2 ,然后返回计算出的值:

 function doubleSubtraction(a, b) { return (a - b) * 2; } 

这些函数具有相似的逻辑,只是它们在传递给它们的数字上执行哪种操作不同。 如果我们可以将函数视为值并将它们作为参数传递给其他函数,则意味着我们可以创建一个函数,该函数接受并使用另一个描述计算功能的函数。 这些考虑因素使我们可以达到以下构造:

 function sum(a, b) { return a + b; } function subtraction(a, b) { return a - b; } function doubleOperator(f, a, b) { return f(a, b) * 2; } doubleOperator(sum, 3, 1); // 8 doubleOperator(subtraction, 3, 1); // 4 

如您所见,现在doubleOperator()函数具有参数f ,并且它表示的函数用于处理参数ab 。 实际上,传递给doubleOperator()函数的sum()doubleOperator()函数允许您控制doubleOperator()函数的行为,并根据其中实现的逻辑对其进行更改。

高阶函数


说到高阶函数,我们指的是具有以下至少一项特征的函数:

  • 一个函数将另一个函数作为参数(可能有多个这样的函数)。
  • 该函数返回另一个函数作为其工作的结果。

您可能已经熟悉标准的JS数组方法filter()map()reduce() 。 让我们谈谈他们。

▍过滤数组和filter()方法


假设我们有一个元素的特定集合,我们希望通过该集合的元素的某些属性对其进行过滤,并形成一个新的集合。 filter()函数期望接收一些用于评估元素的标准,并以此为依据来确定是否在结果集合中包含元素。 此条件由传递给它的函数定义,如果filter()函数应在最终集合中包含一个元素,则返回false ,否则返回false

想象一下,我们有一个整数数组,我们想通过获取一个仅包含原始数组中偶数个数字的新数组来对其进行过滤。

势在必行


在应用命令式方法来使用JavaScript解决此问题时,我们需要实现以下操作序列:

  • 为新元素创建一个空数组(我们称其为evenNumbers )。
  • 遍历原始整数数组(我们称其为numbers )。
  • 将在numbers数组中找到的偶数evenNumbers数组中。

这是该算法的实现形式:

 var numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; var evenNumbers = []; for (var i = 0; i < numbers.length; i++) { if (numbers[i] % 2 == 0) {   evenNumbers.push(numbers[i]); } } console.log(evenNumbers); // (6) [0, 2, 4, 6, 8, 10] 

另外,我们可以编写一个函数(让我们称它为even() ),如果数字为偶数,则返回true ,如果为奇数,则返回false ,然后将其传递给filter()数组方法,该方法通过与之检查数组的每个元素,将形成一个仅包含偶数的新数组:

 function even(number) { return number % 2 == 0; } let listOfNumbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; listOfNumbers.filter(even); // [0, 2, 4, 6, 8, 10] 

顺便说一下,这里是一个有关数组过滤的有趣问题的解决方案,我在Hacker Rank上从事函数式编程任务时就完成了该问题。 根据问题的情况,有必要滤除整数数组,仅显示小于x的给定值的那些元素。

JavaScript中针对此问题的命令性解决方案可能看起来像这样:

 var filterArray = function(x, coll) { var resultArray = []; for (var i = 0; i < coll.length; i++) {   if (coll[i] < x) {     resultArray.push(coll[i]);   } } return resultArray; } console.log(filterArray(3, [10, 9, 8, 2, 7, 5, 1, 3, 0])); // (3) [2, 1, 0] 

命令式方法的本质是,我们概述了功能执行的动作序列。 即,我们描述了数组的搜索,将数组的当前元素与x进行比较,如果通过测试,则将该元素放置在resultArray数组中。

声明式方法


如何切换到声明性方法来解决此问题,并相应使用filter()方法(这是一个高阶函数)? 例如,它可能看起来像这样:

 function smaller(number) { return number < this; } function filterArray(x, listOfNumbers) { return listOfNumbers.filter(smaller, x); } let numbers = [10, 9, 8, 2, 7, 5, 1, 3, 0]; filterArray(3, numbers); // [2, 1, 0] 

在此示例中,您可能会发现在smaller()函数中使用this很不寻常,但是这里没有什么复杂的。 thisfilter()方法的第二个参数。 在我们的示例中,这是filterArray()x参数表示的数字3 。 该数字this表示。

如果数组包含结构相当复杂的实体(例如对象),则可以使用相同的方法。 假设我们有一个存储对象的数组,这些对象包含由name属性表示的人的name ,以及由age属性表示的有关这些人的age 。 这是数组的样子:

 let people = [ { name: "TK", age: 26 }, { name: "Kaio", age: 10 }, { name: "Kazumi", age: 30 } ]; 

我们希望通过仅从年龄超过21岁的人群中选择那些对象来过滤该数组。 解决此问题的方法如下:

 function olderThan21(person) { return person.age > 21; } function overAge(people) { return people.filter(olderThan21); } overAge(people); // [{ name: 'TK', age: 26 }, { name: 'Kazumi', age: 30 }] 

在这里,我们有一个数组,其中包含代表人的对象。 我们使用olderThan21()函数检查此数组的元素。 在这种情况下,我们在检查时参考每个元素的age属性,检查该属性的值是否超过21 。 我们将此函数传递给filter()方法,该方法对数组进行过滤。

▍处理数组元素和map()方法


map()方法用于转换数组元素。 他将传递的函数应用于数组的每个元素,然后构建一个由更改的元素组成的新数组。

让我们继续使用您已经知道的people数组进行实验。 现在,我们将不再基于age对象的属性来过滤此数组。 我们需要在此基础上创建TK is 26 years old格式TK is 26 years old的行列表。 在这种方法中,将根据模板p.name is p.age years old的元素构成的行,其中p.namep.agepeople数组元素的相应属性的值。

解决JavaScript中此问题的一种必要方法如下所示:

 var people = [ { name: "TK", age: 26 }, { name: "Kaio", age: 10 }, { name: "Kazumi", age: 30 } ]; var peopleSentences = []; for (var i = 0; i < people.length; i++) { var sentence = people[i].name + " is " + people[i].age + " years old"; peopleSentences.push(sentence); } console.log(peopleSentences); // ['TK is 26 years old', 'Kaio is 10 years old', 'Kazumi is 30 years old'] 

如果采用声明性方法,则会得到以下信息:

 function makeSentence(person) { return `${person.name} is ${person.age} years old`; } function peopleSentences(people) { return people.map(makeSentence); } peopleSentences(people); // ['TK is 26 years old', 'Kaio is 10 years old', 'Kazumi is 30 years old'] 

实际上,这里的主要思想是您需要对原始数组的每个元素进行处理,然后将其放置在新数组中。

这是Hacker Rank的另一项任务,致力于更新列表 。 即,我们正在谈论将现有数值数组的元素的值更改为其绝对值。 因此,例如,在处理数组[1, 2, 3, -4, 5]由于-4的绝对值为4 [1, 2, 3, -4, 5]因此将采用[1, 2, 3, 4, 5] [1, 2, 3, -4, 5]的形式。

这是当我们迭代数组并将其元素的值更改为其绝对值时,此问题的简单解决方案的示例。

 var values = [1, 2, 3, -4, 5]; for (var i = 0; i < values.length; i++) { values[i] = Math.abs(values[i]); } console.log(values); // [1, 2, 3, 4, 5] 

在这里,要转换数组元素的值,请使用Math.abs()方法,将更改后的元素写入转换之前的相同位置。

.

, , , . . , , , .

, , map() . ?

, abs() , , .

 Math.abs(-1); // 1 Math.abs(1); // 1 Math.abs(-2); // 2 Math.abs(2); // 2 

, , .

, , Math.abs() map() . , ? map() . :

 let values = [1, 2, 3, -4, 5]; function updateListMap(values) { return values.map(Math.abs); } updateListMap(values); // [1, 2, 3, 4, 5] 

, , , , , .

▍ reduce()


reduce() .

. , -. Product 1 , Product 2 , Product 3 Product 4 . .

, . 例如,它可能看起来像这样:

 var orders = [ { productTitle: "Product 1", amount: 10 }, { productTitle: "Product 2", amount: 30 }, { productTitle: "Product 3", amount: 20 }, { productTitle: "Product 4", amount: 60 } ]; var totalAmount = 0; for (var i = 0; i < orders.length; i++) { totalAmount += orders[i].amount; } console.log(totalAmount); // 120 

reduce() , ( sumAmount() ), , reduce() :

 let shoppingCart = [ { productTitle: "Product 1", amount: 10 }, { productTitle: "Product 2", amount: 30 }, { productTitle: "Product 3", amount: 20 }, { productTitle: "Product 4", amount: 60 } ]; const sumAmount = (currentTotalAmount, order) => currentTotalAmount + order.amount; function getTotalAmount(shoppingCart) { return shoppingCart.reduce(sumAmount, 0); } getTotalAmount(shoppingCart); // 120 

shoppingCart , , sumAmount() , ( order , amount ), — currentTotalAmount .

reduce() , getTotalAmount() , sumAmount() , 0 .

map() reduce() . «»? , map() shoppingCart , amount , reduce() sumAmount() . :

 const getAmount = (order) => order.amount; const sumAmount = (acc, amount) => acc + amount; function getTotalAmount(shoppingCart) { return shoppingCart   .map(getAmount)   .reduce(sumAmount, 0); } getTotalAmount(shoppingCart); // 120 

getAmount() amount . map() , , , [10, 30, 20, 60] . , reduce() , .

▍ filter(), map() reduce()


, , filter() , map() reduce() . , , .

-. , :

 let shoppingCart = [ { productTitle: "Functional Programming", type: "books", amount: 10 }, { productTitle: "Kindle", type: "eletronics", amount: 30 }, { productTitle: "Shoes", type: "fashion", amount: 20 }, { productTitle: "Clean Code", type: "books", amount: 60 } ] 

. :

  • type , , books .
  • , .
  • .

, :

 let shoppingCart = [ { productTitle: "Functional Programming", type: "books", amount: 10 }, { productTitle: "Kindle", type: "eletronics", amount: 30 }, { productTitle: "Shoes", type: "fashion", amount: 20 }, { productTitle: "Clean Code", type: "books", amount: 60 } ] const byBooks = (order) => order.type == "books"; const getAmount = (order) => order.amount; const sumAmount = (acc, amount) => acc + amount; function getTotalAmount(shoppingCart) { return shoppingCart   .filter(byBooks)   .map(getAmount)   .reduce(sumAmount, 0); } getTotalAmount(shoppingCart); // 70 

总结


JavaScript-. , .

亲爱的读者们! ?



Source: https://habr.com/ru/post/zh-CN434112/


All Articles