Maison >interface Web >js tutoriel >Une analyse approfondie des fonctions d'ordre supérieur, du curry et de la combinaison de fonctions en JavaScript

Une analyse approfondie des fonctions d'ordre supérieur, du curry et de la combinaison de fonctions en JavaScript

青灯夜游
青灯夜游avant
2021-09-22 20:10:452069parcourir

Cet article vous présentera la programmation fonctionnelle en JavaScript, présentera les fonctions d'ordre supérieur, les fonctions de curry et de combinaison, ainsi que les fonctions fonctionnelles courantes. J'espère qu'il vous sera utile !

Une analyse approfondie des fonctions d'ordre supérieur, du curry et de la combinaison de fonctions en JavaScript

Programmation orientée objet et Programmation fonctionnelle sont deux paradigmes de programmation très différents avec leurs propres règles, avantages et inconvénients.

Cependant, JavaScript ne suit pas toujours une règle, mais se situe en plein milieu de ces deux règles. Il fournit certains aspects des langages POO ordinaires, tels que les classes, les objets, l'héritage, etc. Mais en même temps, il vous donne également quelques concepts de programmation fonctionnelle, tels que les fonctions d'ordre supérieur et la possibilité de les composer.

Fonctions d'ordre supérieur

Commençons par le plus important des trois concepts : les fonctions d'ordre supérieur.

Les fonctions d'ordre supérieur signifient que les fonctions ne sont pas seulement celles qui peuvent être définies et appelées à partir du code, vous pouvez réellement les utiliser comme entités allouables. Si vous avez utilisé du JavaScript, ce n'est pas surprenant. L'attribution de fonctions anonymes à des constantes est très courante.

const adder = (a, b) => {
  return a + b
}

La logique ci-dessus n'est pas valable dans de nombreuses autres langues, être capable d'attribuer des fonctions comme des entiers est un outil très utile, en fait la plupart des sujets abordés dans cet article sont des sous-produits de cette fonction.

Avantages des fonctions d'ordre supérieur : comportement encapsulé

Avec les fonctions d'ordre supérieur, nous pouvons non seulement allouer des fonctions comme ci-dessus, mais également les transmettre en tant que paramètres lorsque la fonction est appelée. Cela ouvre la porte à la création d’une base de code constamment dynamique sur laquelle des comportements complexes peuvent être réutilisés en le passant directement en paramètre.

Imaginez que vous travaillez dans un environnement purement orienté objet et que vous souhaitez étendre les fonctionnalités d'une classe pour accomplir une tâche. Dans ce cas, vous pouvez utiliser l'héritage en encapsulant cette logique d'implémentation dans une classe abstraite, puis en l'étendant dans un ensemble de classes d'implémentation. C'est un comportement POO parfait et cela fonctionne, nous :

  • Créons une structure abstraite pour encapsuler notre logique réutilisable
  • Créons une construction secondaire
  • Nous réutilisons la classe d'origine et l'étendons C'est tout

Maintenant, ce que nous voulons, c'est pour réutiliser la logique, nous pouvons simplement extraire la logique réutilisable dans une fonction, puis transmettre cette fonction comme paramètre à n'importe quelle autre fonction. De cette façon, nous pouvons économiser beaucoup de processus de création "passe-partout", puisque nous créons simplement des fonctions. .

Le code ci-dessous montre comment réutiliser la logique du programme en POO.

//Encapsulated behavior封装行为stract class LogFormatter {
  
  format(msg) {
    return Date.now() + "::" + msg
  } 
}

//重用行为
class ConsoleLogger extends LogFormatter {
  
  log(msg) {
    console.log(this.format(msg))
  }  
}

class FileLogger extends LogFormatter {

  log(msg) {
    writeToFileSync(this.logFile, this.format(msg))
  }
}

La deuxième illustration consiste à extraire la logique en fonctions, nous pouvons mélanger et assortir pour créer facilement ce dont nous avons besoin. Vous pouvez continuer à ajouter d'autres fonctionnalités de formatage et d'écriture, puis simplement les mélanger avec une seule ligne de code :

// 泛型行为抽象
function format(msg) {
  return Date.now() + "::" + msg
}

function consoleWriter(msg) {
  console.log(msg)
}

function fileWriter(msg) {
  let logFile = "logfile.log"
  writeToFileSync(logFile, msg)
}

function logger(output, format) {
  return msg => {
    output(format(msg))
  }
}
// 通过组合函数来使用它
const consoleLogger = logger(consoleWriter, format)
const fileLogger = logger(fileWriter, format)

Les deux approches ont des avantages et les deux sont très efficaces, aucune n'est la meilleure. Juste pour montrer la flexibilité de cette approche, nous avons la possibilité de transmettre des comportements (c'est-à-dire des fonctions) comme arguments comme s'il s'agissait de types primitifs (comme des entiers ou des chaînes).

Avantages des fonctions d'ordre supérieur : code concis

Pour cet avantage, un bon exemple est la méthode Array, telle que forEach, map, <code>réduire et ainsi de suite. Dans les langages de programmation non fonctionnels tels que C, parcourir les éléments du tableau et les transformer nécessite l'utilisation d'une boucle for ou d'une autre construction de boucle. Cela nous oblige à écrire du code d'une manière spécifiée, c'est-à-dire que les exigences décrivent le processus dans lequel le cycle se produit. Array方法,例如forEachmapreduce等等。 在非函数式编程语言(例如C)中,对数组元素进行迭代并对其进行转换需要使用for循环或某些其他循环结构。 这就要求我们以指定方式编写代码,就是需求描述循环发生的过程。

let myArray = [1,2,3,4]
let transformedArray = []

for(let i = 0; i < myArray.length; i++) {
  transformedArray.push(myArray[i] * 2) 
}

上面的代码主要做了:

  • 声明一个新变量i,该变量将用作myArray的索引,其值的范围为0myArray的长度
  • 对于i的每个值,将myArray的值在i的位置相乘,并将其添加到transformedArray数组中。

这种方法很有效,而且相对容易理解,然而,这种逻辑的复杂性会随着项目的复杂程度上升而上升,认知负荷也会随之增加。但是,像下面这种方式就更容易阅读:

const double = x => x * 2;

let myArray = [1,2,3,4];
let transformedArray = myArray.map(double);

与第一种方式相比,这种方式更容易阅读,而且由于逻辑隐藏在两个函数(mapdouble)中,因此你不必担心了解它们的工作原理。 你也可以在第一个示例中将乘法逻辑隐藏在函数内部,但是遍历逻辑必须存在,这就增加了一些不必要的阅读阻碍。

柯里化

函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。我们来看个例子:

function adder(a, b) {
  return a + b
}

// 变成
const add10 = x => adder(a, 10)

现在,如果你要做的就是将10添加到一系列值中,则可以调用add10而不是每次都使用相同的第二个参数调用adder。 这个事例看起来比较蠢,但它是体现了 柯里化

function log(msg, msgPrefix, output) {
  output(msgPrefix + msg)
} 

function consoleOutput(msg) {
  console.log(msg)
}

function fileOutput(msg) {
  let filename = "mylogs.log"
  writeFileSync(msg, filename)
}

const logger = msg => log(msg, ">>", consoleOutput);
const fileLogger = msg => log(msg, "::", fileOutput);

Le code ci-dessus fait principalement : 🎜🎜🎜Déclarer une nouvelle variable i, qui sera utilisée comme index de myArray, et sa plage de valeurs est 0 à la longueur de myArray 🎜🎜Pour chaque valeur de i, placez la valeur de myArray à i est multipliée et ajoutée au tableau <code>transformedArray. 🎜🎜🎜Cette approche est efficace et relativement facile à comprendre, cependant, la complexité de cette logique augmente à mesure que la complexité du projet augmente, tout comme la charge cognitive. Cependant, c'est plus facile à lire comme ceci : 🎜
var add10 = function(value) {
    return value + 10;
};
var mult5 = function(value) {
    return value * 5;
};
🎜 Par rapport à la première façon, cette façon est plus facile à lire, et comme la logique est cachée dans deux fonctions (map et double), vous n’avez donc pas à vous soucier de comprendre comment ils fonctionnent. Vous pouvez également masquer la logique de multiplication à l'intérieur de la fonction dans le premier exemple, mais la logique de parcours doit être présente, ce qui ajoute une obstruction inutile à la lecture. 🎜<h2 id="item-2">🎜Currying🎜🎜🎜🎜Le currying de fonction🎜 consiste à transformer une fonction qui accepte plusieurs paramètres en une fonction qui accepte un seul paramètre (le premier paramètre de la fonction d'origine et renvoie un). nouvelle fonction qui accepte les paramètres restants et renvoie un résultat. Regardons un exemple : 🎜<pre class="brush:js;toolbar:false;">var add10 = value =&gt; value + 10; var mult5 = value =&gt; value * 5;</pre>🎜Maintenant, si tout ce que vous voulez faire est d'ajouter <code>10 à une plage de valeurs, vous pouvez appeler add10 au lieu de l'utiliser à chaque fois. Le même deuxième argument appelle adder. Cet exemple peut paraître idiot, mais il incarne les idéaux du currying. 🎜

你可以将柯里化视为函数式编程的继承,然后按照这种思路再回到logger的示例,可以得到以下内容:

function log(msg, msgPrefix, output) {
  output(msgPrefix + msg)
} 

function consoleOutput(msg) {
  console.log(msg)
}

function fileOutput(msg) {
  let filename = "mylogs.log"
  writeFileSync(msg, filename)
}

const logger = msg => log(msg, ">>", consoleOutput);
const fileLogger = msg => log(msg, "::", fileOutput);

log的函数需要三个参数,而我们将其引入仅需要一个参数的专用版本中,因为其他两个参数已由我们选择。

注意,这里将log函数视为抽象类,只是因为在我的示例中,不想直接使用它,但是这样做是没有限制的,因为这只是一个普通的函数。 如果我们使用的是类,则将无法直接实例化它。

组合函数

函数组合就是组合两到多个函数来生成一个新函数的过程。将函数组合在一起,就像将一连串管道扣合在一起,让数据流过一样。

在计算机科学中,函数组合是将简单函数组合成更复杂函数的一种行为或机制。就像数学中通常的函数组成一样,每个函数的结果作为下一个函数的参数传递,而最后一个函数的结果是整个函数的结果

这是来自维基百科的函数组合的定义,粗体部分是比较关键的部分。使用柯里化时,就没有该限制,我们可以轻松使用预设的函数参数。

代码重用听起来很棒,但是实现起来很难。如果代码业务性过于具体,就很难重用它。如时代码太过通用简单,又很少人使用。所以我们需要平衡两者,一种制作更小的、可重用的部件的方法,我们可以将其作为构建块来构建更复杂的功能。

在函数式编程中,函数是我们的构建块。每个函数都有各自的功能,然后我们把需要的功能(函数)组合起来完成我们的需求,这种方式有点像乐高的积木,在编程中我们称为 组合函数。

看下以下两个函数:

var add10 = function(value) {
    return value + 10;
};
var mult5 = function(value) {
    return value * 5;
};

上面写法有点冗长了,我们用箭头函数改写一下:

var add10 = value => value + 10;
var mult5 = value => value * 5;

现在我们需要有个函数将传入的参数先加上 10 ,然后在乘以 5, 如下:

现在我们需要有个函数将传入的参数先加上 10 ,然后在乘以 5, 如下:

var mult5AfterAdd10 = value => 5 * (value + 10)

尽管这是一个非常简单的例子,但仍然不想从头编写这个函数。首先,这里可能会犯一个错误,比如忘记括号。第二,我们已经有了一个加 10 的函数  add10 和一个乘以 5 的函数 mult5 ,所以这里我们就在写已经重复的代码了。

使用函数 add10mult5 来重构 mult5AfterAdd10

var mult5AfterAdd10 = value => mult5(add10(value));

我们只是使用现有的函数来创建 mult5AfterAdd10,但是还有更好的方法。

在数学中, f ∘ g 是函数组合,叫作“f 由 g 组合”,或者更常见的是 “f after g”。 因此 (f ∘ g)(x) 等效于f(g(x)) 表示调用 g 之后调用 f

在我们的例子中,我们有 mult5 ∘ add10 或 “add10 after mult5”,因此我们的函数的名称叫做 mult5AfterAdd10。由于Javascript本身不做函数组合,看看 Elm 是怎么写的:

add10 value =
    value + 10
mult5 value =
    value * 5
mult5AfterAdd10 value =
    (mult5 << add10) value

Elm 中 << 表示使用组合函数,在上例中 value 传给函数 add10 然后将其结果传递给 mult5。还可以这样组合任意多个函数:

f x =
   (g << h << s << r << t) x

这里 x 传递给函数 t,函数 t 的结果传递给 r,函数 t 的结果传递给 s,以此类推。在Javascript中做类似的事情,它看起来会像 g(h(s(r(t(x))))),一个括号噩梦。

常见的函数式函数(Functional Function)

函数式语言中3个常见的函数:Map,Filter,Reduce

如下JavaScript代码:

 for (var i = 0; i < something.length; ++i) {
    // do stuff
 }

这段代码存在一个很大的问题,但不是bug。问题在于它有很多重复代码(boilerplate code)。如果你用命令式语言来编程,比如Java,C#,JavaScript,PHP,Python等等,你会发现这样的代码你写地最多。这就是问题所在

现在让我们一步一步的解决问题,最后封装成一个看不见 for 语法函数:

先用名为 things 的数组来修改上述代码:

var things = [1, 2, 3, 4];
for (var i = 0; i < things.length; ++i) {
    things[i] = things[i] * 10; // 警告:值被改变!
}
console.log(things); // [10, 20, 30, 40]

这样做法很不对,数值被改变了!

在重新修改一次:

var things = [1, 2, 3, 4];
var newThings = [];
for (var i = 0; i < things.length; ++i) {
    newThings[i] = things[i] * 10;
}
console.log(newThings); // [10, 20, 30, 40]

这里没有修改things数值,但却却修改了newThings。暂时先不管这个,毕竟我们现在用的是 JavaScript。一旦使用函数式语言,任何东西都是不可变的。

现在将代码封装成一个函数,我们将其命名为 map,因为这个函数的功能就是将一个数组的每个值映射(map)到新数组的一个新值。

var map = (f, array) => {
    var newArray = [];
    for (var i = 0; i < array.length; ++i) {
        newArray[i] = f(array[i]);
    }
    return newArray;
};

函数 f 作为参数传入,那么函数 map 可以对 array 数组的每项进行任意的操作。

现在使用 map 重写之前的代码:

var things = [1, 2, 3, 4];
var newThings = map(v => v * 10, things);

这里没有 for 循环!而且代码更具可读性,也更易分析。

现在让我们写另一个常见的函数来过滤数组中的元素:

var filter = (pred, array) => {
    var newArray = [];
for (var i = 0; i < array.length; ++i) {
        if (pred(array[i]))
            newArray[newArray.length] = array[i];
    }
    return newArray;
};

当某些项需要被保留的时候,断言函数 pred 返回TRUE,否则返回FALSE。

使用过滤器过滤奇数:

var isOdd = x => x % 2 !== 0;
var numbers = [1, 2, 3, 4, 5];
var oddNumbers = filter(isOdd, numbers);
console.log(oddNumbers); // [1, 3, 5]

比起用 for 循环的手动编程,filter 函数简单多了。最后一个常见函数叫reduce。通常这个函数用来将一个数列归约(reduce)成一个数值,但事实上它能做很多事情。

在函数式语言中,这个函数称为 fold

var reduce = (f, start, array) => {
    var acc = start;
    for (var i = 0; i < array.length; ++i)
        acc = f(array[i], acc); // f() 有2个参数
    return acc;
});

reduce函数接受一个归约函数 f,一个初始值 start,以及一个数组 array

这三个函数,map,filter,reduce能让我们绕过for循环这种重复的方式,对数组做一些常见的操作。但在函数式语言中只有递归没有循环,这三个函数就更有用了。附带提一句,在函数式语言中,递归函数不仅非常有用,还必不可少。

英文原文地址:https://blog.bitsrc.io/functional-programming-in-functions-composition-and-currying-3c765a50152e

作者:Fernando Doglio

更多编程相关知识,请访问:编程视频!!

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer