Heim  >  Artikel  >  Web-Frontend  >  Eine eingehende Analyse von Funktionen höherer Ordnung, Currying- und Kombinationsfunktionen in JavaScript

Eine eingehende Analyse von Funktionen höherer Ordnung, Currying- und Kombinationsfunktionen in JavaScript

青灯夜游
青灯夜游nach vorne
2021-09-22 20:10:451967Durchsuche

Dieser Artikel führt Sie in die funktionale Programmierung in JavaScript ein, stellt Funktionen höherer Ordnung, Curry- und Kombinationsfunktionen sowie allgemeine Funktionsfunktionen vor. Ich hoffe, er wird Ihnen hilfreich sein!

Eine eingehende Analyse von Funktionen höherer Ordnung, Currying- und Kombinationsfunktionen in JavaScript

Objektorientierte Programmierung und Funktionale Programmierung sind zwei sehr unterschiedliche Programmierparadigmen mit ihren eigenen Regeln, Vor- und Nachteilen.

JavaScript folgt jedoch nicht immer einer Regel, sondern liegt genau in der Mitte dieser beiden Regeln. Es bietet einige Aspekte gewöhnlicher OOP-Sprachen, wie Klassen, Objekte, Vererbung usw. Gleichzeitig werden Ihnen aber auch einige Konzepte der funktionalen Programmierung vermittelt, beispielsweise Funktionen höherer Ordnung und die Möglichkeit, diese zu erstellen.

Funktionen höherer Ordnung

Beginnen wir mit dem wichtigsten der drei Konzepte: Funktionen höherer Ordnung.

Funktionen höherer Ordnung bedeuten, dass Funktionen nicht nur aus Code definiert und aufgerufen werden können, sondern dass Sie sie tatsächlich als zuweisbare Entitäten verwenden können. Wenn Sie JavaScript verwendet haben, ist dies nicht überraschend. Das Zuweisen anonymer Funktionen zu Konstanten ist weit verbreitet.

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

Die obige Logik ist in vielen anderen Sprachen nicht gültig. Die Zuweisung von Funktionen wie Ganzzahlen ist ein sehr nützliches Werkzeug. Tatsächlich sind die meisten in diesem Artikel behandelten Themen Nebenprodukte dieser Funktion.

Vorteile von Funktionen höherer Ordnung: gekapseltes Verhalten

Mit Funktionen höherer Ordnung können wir Funktionen nicht nur wie oben zuweisen, sondern sie auch als Parameter übergeben, wenn die Funktion aufgerufen wird. Dies öffnet die Tür zur Schaffung einer ständig dynamischen Codebasis, auf der komplexes Verhalten durch direkte Übergabe als Parameter wiederverwendet werden kann.

Stellen Sie sich vor, Sie arbeiten in einer rein objektorientierten Umgebung und möchten die Funktionalität einer Klasse erweitern, um eine Aufgabe zu erledigen. In diesem Fall könnten Sie die Vererbung nutzen, indem Sie diese Implementierungslogik in einer abstrakten Klasse kapseln und sie dann auf eine Reihe von Implementierungsklassen erweitern. Das ist perfektes OOP-Verhalten und es funktioniert. Wir:

  • Erstellen Sie eine abstrakte Struktur, um unsere wiederverwendbare Logik zu kapseln.
  • Erstellen Sie ein sekundäres Konstrukt.
  • Wir verwenden die ursprüngliche Klasse wieder und erweitern sie. Das war's.

Was wir jetzt wollen, ist Um die Logik wiederzuverwenden, können wir die wiederverwendbare Logik einfach in eine Funktion extrahieren und diese Funktion dann als Parameter an eine andere Funktion übergeben. Auf diese Weise können wir viel Erstellungsaufwand einsparen. Ein „Boilerplate“-Prozess, da wir nur Funktionen erstellen .

Der folgende Code zeigt, wie Programmlogik in OOP wiederverwendet wird.

//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))
  }
}

Die zweite Illustration besteht darin, die Logik in Funktionen zu extrahieren, die wir kombinieren und kombinieren können, um auf einfache Weise das zu erstellen, was wir brauchen. Sie können weitere Formatierungs- und Schreibfunktionen hinzufügen und diese dann einfach mit einer einzigen Codezeile kombinieren:

// 泛型行为抽象
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)

Beide Ansätze haben Vorteile und beide sind sehr effektiv, keiner ist der Beste. Um die Flexibilität dieses Ansatzes zu verdeutlichen, haben wir die Möglichkeit, Verhaltensweisen (d. h. Funktionen) als Argumente zu übergeben, als wären sie primitive Typen (wie Ganzzahlen oder Zeichenfolgen).

Vorteile von Funktionen höherer Ordnung: prägnanter Code

Ein gutes Beispiel für diesen Vorteil ist die Methode Array, z. B. forEach, map, <code>reduce und so weiter. In nicht-funktionalen Programmiersprachen wie C erfordert die Iteration über Array-Elemente und deren Transformation die Verwendung einer for-Schleife oder eines anderen Schleifenkonstrukts. Dies erfordert, dass wir Code auf eine bestimmte Art und Weise schreiben, das heißt, die Anforderungen beschreiben den Prozess, in dem der Zyklus stattfindet. 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);

Der obige Code führt hauptsächlich Folgendes aus: 🎜🎜🎜Deklarieren Sie eine neue Variable i, die als Index von myArray verwendet wird und deren Wertebereich ist 0 auf die Länge von myArray 🎜🎜Platzieren Sie für jeden Wert von i den Wert von myArray bei i wird multipliziert und zum Array <code>transformedArray hinzugefügt. 🎜🎜🎜Dieser Ansatz ist effektiv und relativ leicht zu verstehen, jedoch nimmt die Komplexität dieser Logik mit zunehmender Komplexität des Projekts zu und damit auch die kognitive Belastung. Es ist jedoch einfacher, es so zu lesen: 🎜
var add10 = function(value) {
    return value + 10;
};
var mult5 = function(value) {
    return value * 5;
};
🎜 Im Vergleich zum ersten Weg ist dieser Weg einfacher zu lesen, und da die Logik in zwei Funktionen verborgen ist (map und double), sodass Sie sich keine Gedanken darüber machen müssen, wie sie funktionieren. Sie könnten die Multiplikationslogik im ersten Beispiel auch innerhalb der Funktion verbergen, aber die Traversierungslogik muss vorhanden sein, was zu unnötigen Lesehindernissen führt. 🎜<h2 id="item-2">🎜Currying🎜🎜🎜🎜Funktions-Currying🎜 besteht darin, eine Funktion, die mehrere Parameter akzeptiert, in eine Funktion umzuwandeln, die einen einzelnen Parameter (den ersten Parameter der ursprünglichen Funktion) akzeptiert und a zurückgibt neue Funktion, die die restlichen Parameter akzeptiert und ein Ergebnis zurückgibt. Schauen wir uns ein Beispiel an: 🎜<pre class="brush:js;toolbar:false;">var add10 = value =&gt; value + 10; var mult5 = value =&gt; value * 5;</pre>🎜Wenn Sie nun nur <code>10 zu einem Wertebereich hinzufügen möchten, können Sie add10 aufrufen, anstatt es jedes Mal zu verwenden Das gleiche zweite Argument ruft adder auf. Dieses Beispiel mag albern erscheinen, aber es verkörpert die Ideale des 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

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

Das obige ist der detaillierte Inhalt vonEine eingehende Analyse von Funktionen höherer Ordnung, Currying- und Kombinationsfunktionen in JavaScript. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:segmentfault.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen