Funktionale Programmierung aus Sicht von EcmaScript. Zusammensetzung, Currying, Teilanwendung

Hallo habr

Heute setzen wir unsere Forschung zur funktionalen Programmierung im Kontext von EcmaScript fort, dessen Spezifikation auf JavaScript basiert. Im vorigen Artikel haben wir die Grundkonzepte untersucht: reine Funktionen, Lambdas, das Konzept der Immunität. Heute sprechen wir über etwas komplexere FP-Techniken: Komposition, Curry und reine Funktionen. Der Artikel ist im Stil einer "Pseudo-Codevorschau" geschrieben, d.h. Wir werden ein praktisches Problem lösen und gleichzeitig die Konzepte der Phasenübergänge und des Refactor-Codes untersuchen, um diesen mit den Idealen der Phasenübergänge in Einklang zu bringen.

Also fangen wir an!

Angenommen, wir haben eine Aufgabe: eine Reihe von Werkzeugen für die Arbeit mit Palindromen zu erstellen.
Palindrom
Männliches Geschlecht
Ein Wort oder eine Phrase, die in gleicher Weise von links nach rechts und von rechts nach links gelesen wird.
"P. "Ich gehe mit dem Schwert des Richters"
Eine der möglichen Implementierungen dieser Aufgabe könnte folgendermaßen aussehen:

function getPalindrom (str) { const regexp = /[\.,\/#!$%\^&\*;:{}=\-_`~()?\s]/g; str = str.replace(regexp, '').toLowerCase().split('').reverse().join(''); // -       ,       return str; } function isPalindrom (str) { const regexp = /[\.,\/#!$%\^&\*;:{}=\-_`~()?\s]/g; str = str.replace(regexp, '').toLowerCase(); return str === str.split('').reverse().join(''); } 

Natürlich funktioniert diese Implementierung. Wir können davon ausgehen, dass getPalindrom korrekt funktioniert, wenn die API die richtigen Daten zurückgibt. Ein Aufruf von isPalindrom ("Ich gehe mit einem Schwertrichter") gibt "wahr" zurück, und ein Aufruf von isPalindrom ("kein Palindrom") gibt "falsch" zurück. Ist diese Implementierung in Bezug auf Ideale der funktionalen Programmierung gut? Auf keinen Fall gut!

Gemäß der Definition von Pure Functions aus diesem Artikel :
Reine Funktionen (PF) - geben immer ein vorhergesagtes Ergebnis zurück.
PF-Eigenschaften:

Das Ergebnis der PF-Ausführung hängt nur von den übergebenen Argumenten und dem Algorithmus ab, der PF implementiert
Verwenden Sie keine globalen Werte
Ändern Sie keine externen Werte oder übergebenen Argumente
Schreiben Sie keine Daten in Dateien, Datenbanken oder anderswo
Und was sehen wir in unserem Beispiel mit Palindromen?

Erstens gibt es eine Vervielfältigung von Code, d.h. das Prinzip von DRY wird verletzt. Zweitens greift die Funktion getPalindrom auf die Datenbank zu. Drittens ändern Funktionen ihre Argumente. Insgesamt sind unsere Funktionen nicht sauber.

Erinnern Sie sich an die Definition: Funktionale Programmierung ist eine Möglichkeit, Code durch Kompilieren einer Reihe von Funktionen zu schreiben.

Wir stellen eine Reihe von Funktionen für diese Aufgabe zusammen:

 const allNotWordSymbolsRegexpGlobal = () => /[\.,\/#!$%\^&\*;:{}=\-_~()?\s]/g;//(1) const replace = (regexp, replacement, str) => str.replace(regexp, replacement);//(2) const toLowerCase = str => str.toLowerCase();//(3) const stringReverse = str => str.split('').reverse().join('');//(4) const isStringsEqual = (strA, strB) => strA === strB;//(5) 

In Zeile 1 haben wir die Konstante des regulären Ausdrucks in funktionaler Form deklariert. Diese Methode zur Beschreibung von Konstanten wird häufig in FP verwendet. In Zeile 2 haben wir die String.prototype.replace-Methode in eine funktionale Ersetzungsabstraktion gekapselt, sodass sie (der Ersetzungsaufruf) dem Vertrag für die funktionale Programmierung entspricht. In Zeile 3 wurde auf die gleiche Weise eine Abstraktion für String.prototype.toLowerCase erstellt. In der vierten implementierten sie eine Funktion, die aus der übergebenen eine neue erweiterte Zeichenfolge erstellt. 5. Überprüft die Stringgleichheit.

Bitte beachten Sie, dass unsere Funktionen äußerst sauber sind! Wir haben in einem früheren Artikel über die Vorteile reiner Funktionen gesprochen.

Jetzt müssen wir überprüfen, ob es sich bei der Zeichenfolge um ein Palindrom handelt. Eine Zusammenstellung von Funktionen wird uns dabei helfen.

Die Zusammensetzung von Funktionen ist die Vereinigung von zwei oder mehr Funktionen zu einer bestimmten resultierenden Funktion, die das Verhalten derjenigen implementiert, die in der gewünschten algorithmischen Sequenz kombiniert sind.

Die Definition mag kompliziert erscheinen, ist aber aus praktischer Sicht fair.

Wir können das machen:

 isStringsEqual(toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', '    ')), stringReverse(toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', '    ')))); 

oder so:

 const strA = toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', '    ')); const strB = stringReverse(toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', '    '))); console.log(isStringsEqual(strA, strB)); 

oder geben Sie für jeden Schritt des implementierten Algorithmus eine weitere Gruppe erklärender Variablen ein. Ein solcher Code ist häufig in Projekten zu sehen, und dies ist ein typisches Beispiel für die Komposition: Sie übergeben einen Aufruf an eine Funktion als Argument an eine andere. Trotzdem ist dieser Ansatz, wie wir sehen, in einer Situation, in der es viele Funktionen gibt, schlecht, weil Dieser Code ist nicht lesbar! Also was jetzt? Sind wir nicht einverstanden mit der funktionalen Programmierung?

Tatsächlich müssen wir, wie es normalerweise bei der funktionalen Programmierung der Fall ist, nur eine weitere Funktion schreiben.

 const compose = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x); 

Die Funktion compose verwendet eine Liste ausführbarer Funktionen als Argumente, wandelt sie in ein Array um, speichert sie in einem Closure und gibt eine Funktion zurück, die einen Anfangswert erwartet. Nachdem der Anfangswert übergeben wurde, beginnt die sequentielle Ausführung aller Funktionen aus dem fns-Array. Das Argument der ersten Funktion ist der übergebene Anfangswert x, und die Argumente aller nachfolgenden Funktionen sind das Ergebnis der vorherigen. So können wir Kompositionen mit beliebig vielen Funktionen erstellen.

Beim Erstellen funktionaler Kompositionen ist es sehr wichtig, die Typen der Eingabeparameter und die Rückgabewerte der einzelnen Funktionen zu überwachen, damit keine unerwarteten Fehler auftreten Wir übergeben das Ergebnis der vorherigen Funktion an die nächste.

Bereits jetzt sehen wir jedoch Probleme bei der Anwendung der Kompositionstechnik auf unseren Code, da die Funktion:

 const replace = (regexp, replacement, str) => str.replace(regexp, replacement); 

Erwartet, dass 3 Eingabeparameter akzeptiert werden, und wir senden nur einen zum Komponieren. Eine andere FP-Technik, Currying, wird uns helfen, dieses Problem zu lösen.

Currying ist die Umwandlung einer Funktion aus vielen Argumenten in eine Funktion aus einem Argument.

Erinnern Sie sich an unsere Add-Funktion aus dem ersten Artikel?

 const add = (x,y) => x+y; 

Es kann so gewechselt werden:

 const add = x => y => x+y; 

Die Funktion nimmt x und gibt ein Lambda zurück, das y erwartet und die Aktion ausführt.

Curry Vorteile:

  • der Code sieht besser aus;
  • Curry-Funktionen sind immer sauber.

Jetzt transformieren wir unsere Ersetzungsfunktion so, dass sie nur ein Argument akzeptiert. Da wir die Funktion benötigen, um Zeichen in der Zeichenfolge durch einen zuvor bekannten regulären Ausdruck zu ersetzen, können wir eine teilweise angewendete Funktion erstellen.

 const replaceAllNotWordSymbolsGlobal = replacement => str => replace(allNotWordSymbolsRegexpGlobal(), replacement, str); 

Wie Sie sehen, korrigieren wir eines der Argumente mit einer Konstante. Dies liegt daran, dass das Currying tatsächlich ein Sonderfall der Teilnutzung ist.

Eine Teilanwendung umschließt eine Funktion mit einem Wrapper, der weniger Argumente akzeptiert als die Funktion selbst. Der Wrapper sollte eine Funktion zurückgeben, die den Rest der Argumente übernimmt.

In unserem Fall haben wir die Funktion replaceAllNotWordSymbolsGlobal erstellt, bei der es sich um eine teilweise angewendete Ersetzungsoption handelt. Es akzeptiert das Ersetzen, speichert es in einem Closure und erwartet eine Eingabezeile, für die es das Ersetzen aufruft, und wir rechnen mit einer Konstanten.

Zurück zu den Palindromen. Erstellen Sie eine Zusammenstellung von Funktionen für das Palindrome-Timing:

 const processFormPalindrom = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, stringReverse ); 

und die Zusammensetzung der Funktionen für die Linie, mit der wir das potentielle Palindrom vergleichen wollen:

 const processFormTestString = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, ); 

Denken Sie jetzt daran, was wir oben gesagt haben:
Ein typisches Kompositionsbeispiel ist die Übergabe eines Aufrufs an eine Funktion als Argument an eine andere
und schreibe:

 const testString = '    ';//          , .. ,    ,  ,   -   ,    const isPalindrom = isStringsEqual(processFormPalindrom(testString), processFormTestString(testString)); 

Hier haben wir eine funktionierende und gut aussehende Lösung:

 const allNotWordSymbolsRegexpGlobal = () => /[\.,\/#!$%\^&\*;:{}=\-_~()?\s]/g; const replace = (regexp, replacement, str) => str.replace(regexp, replacement); const toLowerCase = str => str.toLowerCase(); const stringReverse = str => str.split('').reverse().join(''); const isStringsEqual = (strA, strB) => strA === strB; const replaceAllNotWordSymbolsGlobal = replacement => str => replace(allNotWordSymbolsRegexpGlobal(), replacement, str); const compose = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x); const processFormPalindrom = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, stringReverse ); const processFormTestString = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, ); const testString = '    '; const isPalindrom = isStringsEqual(processFormPalindrom(testString), processFormTestString(testString)); 

Wir möchten jedoch nicht jedes Mal ein Currying durchführen oder teilweise angewandte Funktionen mit unseren Händen erstellen. Natürlich wollen wir nicht, Programmierer sind faule Leute. Aus diesem Grund werden wir, wie in FP üblich, ein paar weitere Funktionen schreiben:

 const curry = fn => (...args) => { if (fn.length > args.length) { const f = fn.bind(null, ...args); return curry(f); } else { return fn(...args) } } 

Die Curry-Funktion übernimmt eine Curry-Funktion, speichert sie in einem Verschluss und gibt ein Lambda zurück. Das Lambda erwartet den Rest der Argumente für die Funktion. Jedes Mal, wenn ein Argument empfangen wird, wird überprüft, ob alle deklarierten Argumente akzeptiert werden. Wenn akzeptiert, wird die Funktion aufgerufen und ihr Ergebnis zurückgegeben. Wenn nicht, wird die Funktion erneut ausgeführt.

Wir können auch eine teilweise angewendete Funktion erstellen, um den benötigten regulären Ausdruck durch eine leere Zeichenfolge zu ersetzen:

 const replaceAllNotWordSymbolsToEmpltyGlobal = curry(replace)(allNotWordSymbolsRegexpGlobal(), ''); 

Alles scheint in Ordnung zu sein, aber wir sind Perfektionisten und wir mögen nicht zu viele Klammern, wir möchten noch besser, also schreiben wir eine andere Funktion oder vielleicht zwei:

 const party = (fn, x) => (...args) => fn(x, ...args); 

Dies ist eine Abstraktionsimplementierung zum Erstellen von teilweise angewendeten Funktionen. Es nimmt eine Funktion und das erste Argument, gibt ein Lambda zurück, das den Rest erwartet und die Funktion ausführt.

Jetzt schreiben wir party neu, damit wir eine teilweise angewendete Funktion mehrerer Argumente erstellen können:

 const party = (fn, ...args) => (...rest) => fn(...args.concat(rest)); 

Es ist erwähnenswert, dass auf diese Weise ausgeführte Funktionen mit einer beliebigen Anzahl von Argumenten aufgerufen werden können, die kleiner als deklariert sind (fn.length).

 const sum = (a,b,c,d) => a+b+c+d; const fn = curry(sum); const r1 = fn(1,2,3,4);//,   const r2 = fn(1, 2, 3)(4);//       const r3 = fn(1, 2)(3)(4); const r4 = fn(1)(2)(3)(4); const r5 = fn(1)(2, 3, 4); const r6 = fn(1)(2)(3, 4); const r7 = fn(1, 2)(3, 4); 

Kommen wir zurück zu unseren Palindromen. Wir können unser replaceAllNotWordSymbolsToEmpltyGlobal ohne zusätzliche Klammern umschreiben:

 const replaceAllNotWordSymbolsToEmpltyGlobal = party(replace,allNotWordSymbolsRegexpGlobal(), ''); 

Schauen wir uns den gesamten Code an:

 //    -       const allNotWordSymbolsRegexpGlobal = () => /[\.,\/#!$%\^&\*;:{}=\-_~()?\s]/g; const replace = (regexp, replacement, str) => str.replace(regexp, replacement); const toLowerCase = str => str.toLowerCase(); const stringReverse = str => str.split('').reverse().join(''); const isStringsEqual = (strA, strB) => strA === strB; //       const testString = '    '; //           -    rambda.js const compose = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x); const curry = fn => (...args) => { if (fn.length > args.length) { const f = fn.bind(null, ...args); return curry(f); } else { return fn(...args) } } const party = (fn, ...args) => (...rest) => fn(...args.concat(rest)); //       const replaceAllNotWordSymbolsToEmpltyGlobal = party(replace,allNotWordSymbolsRegexpGlobal(), ''); const processFormPalindrom = compose( replaceAllNotWordSymbolsToEmpltyGlobal, toLowerCase, stringReverse ); const processFormTestString = compose( replaceAllNotWordSymbolsToEmpltyGlobal, toLowerCase, ); const checkPalindrom = testString => isStringsEqual(processFormPalindrom(testString), processFormTestString(testString)); 

Es sieht gut aus, aber was ist, wenn es für uns kein String ist, sondern ein Array kommt? Deshalb fügen wir eine weitere Funktion hinzu:

 const map = fn => (...args) => args.map(fn); 

Wenn wir nun ein Array zum Testen auf Palindrome haben, dann:

 const palindroms = ['    ','   ','   '. ' '] map(checkPalindrom )(...palindroms ); // [true, true, true, false]   

So haben wir die Aufgabe gelöst, indem wir Feature-Sets geschrieben haben. Achten Sie auf die sinnlose Art, Code zu schreiben - dies ist ein Lackmustest der funktionalen Reinheit.

Jetzt etwas mehr Theorie. Vergessen Sie nicht, dass Sie jedes Mal, wenn Sie eine Funktion aufrufen, eine neue erstellen, d. H. Wählen Sie eine Speicherzelle dafür. Es ist wichtig, dies zu überwachen, um Undichtigkeiten zu vermeiden.

Funktionsbibliotheken wie ramda.js haben Compose- und Pipe-Funktionen. compose implementiert den Kompositionsalgorithmus von rechts nach links und die Pipe von links nach rechts. Unsere Compose-Funktion ist eine Analogie zur Pipe von Ramda. In der Bibliothek gibt es seitdem zwei verschiedene Kompositionsfunktionen Komposition von rechts nach links und von links nach rechts sind zwei verschiedene Verträge der funktionalen Programmierung. Wenn einer der Leser einen Artikel findet, der alle bestehenden Verträge der FP beschreibt, dann teile ihn in den Kommentaren mit, ich werde ihn gerne lesen und dem Kommentar ein Plus hinzufügen!

Die Anzahl der formalen Parameter einer Funktion wird als Arität bezeichnet . Dies ist auch eine wichtige Definition aus Sicht der Theorie der Phasenübergänge.

Fazit


Im Rahmen dieses Artikels haben wir funktionale Programmiertechniken wie Komposition, Currying und Teilanwendung untersucht. Natürlich werden Sie in realen Projekten mit diesen Tools vorgefertigte Bibliotheken verwenden, aber als Teil des Artikels habe ich alles auf native JS implementiert, damit Leser mit vielleicht nicht sehr viel Erfahrung im FP verstehen können, wie diese Techniken unter der Haube funktionieren.

Ich habe mich auch bewusst für die Methode der Narration entschieden - die Pseudo-Codevorschau, um meine Logik zu veranschaulichen, funktionale Reinheit im Code zu erreichen.

Übrigens können Sie die Entwicklung dieses Moduls zur Arbeit mit Palindromen fortsetzen und seine Ideen weiterentwickeln, z. B. Linien per API herunterladen, in Buchstabensätze konvertieren und an den Server senden, auf dem die Linie vom Palindrom generiert wird, und vieles mehr ... Nach Ihrem Ermessen.

Es wäre auch schön, die Duplizierung in den Prozessen dieser Zeilen loszuwerden:

  replaceAllNotWordSymbolsToEmpltyGlobal, toLowerCase, 

Generell ist es möglich und notwendig, den Code ständig zu verbessern!

Bis zu zukünftigen Artikeln.

Source: https://habr.com/ru/post/de475324/


All Articles