
Zu viele JavaScript-Entwickler haben keine Ahnung, wozu JavaScript alles fähig ist.
Dieser Artikel wird die Art und Weise, wie Sie Code schreiben, verändern und Ihr Potenzial als Programmierer entfesseln.
Am Ende dieses Artikels oder des untenstehenden Videos werden Sie in der Lage sein, Code wie diesen zu lesen, zu verstehen und zu schreiben:
const curry =
(f, array = []) =>
(...args) =>
(a => (a.length >= f.length ? f(...a) : curry(f, a)))([
...array,
...args,
]);
const add = curry((a, b) => a + b);
const inc = add(1);
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const doubleInc = pipe(x => x * 2, inc);
JavaScript ist eine Programmiersprache mit zwei Paradigmen, da sie OOP und FP unterstützt. Dieser Artikel ist Ihr „Schritt-für-Schritt-Leitfaden ohne ausgelassene Schritte“ zur funktionalen Programmierung, damit Sie die Sprache in vollem Umfang nutzen und modulareren, deterministischeren und besser testbaren Code schreiben können. Er reicht buchstäblich von Primitiven bis hin zur fortgeschrittenen Funktionskomposition.
Wenn Sie nach funktionaler Programmierung suchen, könnten Sie sich im akademischen Fachjargon verlieren. Mit etwas Glück finden Sie eine einfache Definition wie diese:
“Funktionale Programmierung ist der Prozess der Softwareentwicklung durch das Komponieren von Funktionen. Funktionale Programmierung ist deklarativ und nicht imperativ. Seiteneffekte sind isoliert. Und der Anwendungszustand fließt normalerweise durch reine Funktionen.”
Sie können sich entspannen, denn dieser Artikel wird all diese Begriffe und mehr erklären. Beginnend mit Primitiven.
Primitive sind alle Datentypen, die jeweils nur einen Wert speichern können. JavaScript verfügt über 7 primitive Datentypen.
string: Stellt Text dar.number: Stellt numerische Werte dar.boolean: Stellt wahr oder falsch dar.undefiniert: Zeigt an, dass einer Variablen kein Wert zugewiesen wurde.null: Stellt das absichtliche Fehlen eines Objektwerts dar.Symbol: Erzeugt eindeutige Bezeichner.BigInt: Verarbeitet Zahlen, die größer sind, als der Standard-Zahlentyp verarbeiten kann.// primitives.js
const string = "ReactSquad.io";
const number = 9001;
const boolean = true;
const notThere = undefined;
const weirdUndefined = null;
const noOneUsesThis = Symbol('🤯 ');
const bigInt = 1n;
Zusammengesetzte Datentypen können Sammlungen oder Kombinationen von Werten speichern.
// composite.js
const obj = { key: "value" };
const array = [1, 2, 3, 4, 5];
Eine Map in JavaScript ist eine Sammlung von schlüsselbasierten Datenelementen, die die Reihenfolge der Einfügungen beibehält und Schlüssel beliebigen Typs zulässt.
// map.js
// Creating a new Map
const map = new Map();
// Setting key-value pairs in the Map
map.set('name', 'John');
map.set('age', 30);
map.set(1, 'one');
console.log(map);
// Output: Map(3) {'name' => 'John', 'age' => 30, 1 => 'one'}
// Retrieving a value by key.
console.log(map.get('name')); // Output: John
// Checking if a key exists.
console.log(map.has('age')); // Output: true
// Size of the Map.
console.log(map.size); // Output: 3
Unleash JavaScript's Potential With Functional Programming Article.md 2024-09-30
3 / 29
// Removing a key-value pair.
map.delete(1);
// Clearing all entries.
map.clear();
Ein Set in JavaScript ist eine Sammlung einzigartiger Werte, die sicherstellt, dass jeder Wert nur einmal vorkommt.
// set.js
// Creating a new Set.
const set = new Set();
// Adding values to the Set.
set.add('apple');
set.add('banana');
set.add('apple'); // This will not be added again.
console.log(set);
// Output: Set(2) {'apple', 'banana'}
// Reminder: Sets delete duplicates.
// Checking if a value exists.
console.log(set.has('banana')); // Output: true
// Size of the Set.
console.log(set.size); // Output: 2
// Deleting a value.
set.delete('apple');
// Iterating over Set values.
set.forEach(value => {
console.log(value);
});
// Clearing all values.
set.clear();
function sumDeclaration(a, b) {
return a + b;
}
const sumArrow = (a, b) => a + b;Eine Funktion ist ein Prozess, der Eingaben, sogenannte Parameter, entgegennehmen und eine Ausgabe namens Rückgabewert.
function myFunction(parameter) {
return parameter;
}
const myArgument = "Some value";
myFunction(myArgument); // Output: "Some value";Eine Funktion kann eine Abbildung, eine Prozedur sein oder E/A-Operationen verwalten.
const square = x => x * x;
square(7); // 49
function prepareTea(teaType) {
let steps = [];
steps.push("Boil water");
steps.push("Add " + teaType);
steps.push("Steep for 5 minutes");
steps.push("Serve hot");
return steps.join("\n");
}
console.log(prepareTea("green tea"));
// Output: Boil water
// Add green tea
// Steep for 5 minutes
// Serve hot
function calculateCircleArea(radius) {
if (radius <= 0) {
return "Error: Radius must be greater than zero.";
}
const pi = Math.PI; // Use Math.PI for more accuracy
const squaredRadius = radius * radius;
const area = pi * squaredRadius;
const roundedArea = Math.round(area * 100) / 100; // Round to 2 decimal
places
return "Calculated area: " + roundedArea;
}
console.log(calculateCircleArea(5));
// Output: Calculated area: 78.54
console.log(calculateCircleArea(-1));
// Output: Error: Radius must be greater than zero.
async function fetchData(url) {
const response = await fetch(url);
const data = await response.json();
return data;
}
fetchData('https://jsonplaceholder.typicode.com/todos/1')
.then(data => console.log(data));
// {
// "userId": 1,
// "id": 1,
// "title": "delectus aut autem",
// "completed": false
// }
Methoden sind Funktionen, die an Objekte gebunden sind. Sie ermöglichen es Ihnen, Operationen mit den Daten des Objekts durchzuführen. In JavaScript werden Methoden häufig mit Objekten, Arrays und anderen integrierten Typen verwendet.
const person = {
name: 'John',
greet: function() {
console.log('Hello, ' + this.name);
}
};
// Calling the greet method.
person.greet(); // Output: Hello, John
Weiter oben in diesem Artikel haben Sie gesehen, wie man Methoden der Map und Set Objekte verwendet.
Primitive Datentypen haben ebenfalls Methoden, da im Hintergrund in JavaScript alles ein Objekt ist.
const greeting = 'Hello, world';
// Changing case.
console.log(greeting.toUpperCase()); // Output: HELLO, WORLD
// Replacing part of the string.
console.log(greeting.replace('Hello', 'Goodbye')); // Output: Goodbye,
world
// Checking if it includes a substring.
console.log(greeting.includes('world')); // Output: true
Ein „Noop“ steht für „no operation“ (keine Operation). Es beschreibt eine Funktion, Operation oder einen Befehl, der nichts tut. Dies kann nützlich sein für Platzhalter im Code oder um absichtlich keine Wirkung zu erzielen.
In JavaScript sieht eine noop Funktion so aus:
function noop() {}
const noop = () => {};
Eine Funktion ist eine reine Funktion, wenn:
Reine Funktionen haben Sie bereits kennengelernt. Zum Beispiel sind sowohl die Summenfunktionen als auch die Quadratfunktion rein.
Reine Funktionen sind deterministisch. Dies wird durch die erste Eigenschaft erfasst. Eine reine Funktion erzeugt immer die gleiche Ausgabe für dieselben Eingaben, egal wann oder wie oft sie aufgerufen wird. Diese Vorhersagbarkeit ist eine Schlüsseleigenschaft reiner Funktionen und unerlässlich für zuverlässigen und testbaren Code.
Hier ist ein Beispiel für eine Funktion, die gegen die erste Regel verstößt.
/**
* Generates a random integer between the start and end values, both
inclusive.
*
* @param {number} start - The starting value.
* @param {number} end - The ending value.
* @returns {number} - A random integer between the start and end.
*/
export const generateRandomInt = (start, end) =>
Math.round(Math.random() * (end - start) + start); kann mit denselben
generateRandomIntstart und end Werten aufgerufen werden, aber sie liefert unterschiedliche Ergebnisse, weil sie Math.random().
Und hier ist ein Beispiel, das gegen die zweite Regel verstößt.
let externalArray = [];
function sideEffectingFunction(x) {
externalArray.push(x); // Modifies external array
return x;
}
console.log(sideEffectingFunction(5)); // 5, modifies externalArray
console.log(sideEffectingFunction(10)); // 10, modifies externalArray, too
console.log(externalArray); // Output: [5, 10]Auch wenn sideEffectingFunction etwas zurückgibt, schiebt es als Nebeneffekt Code in ein Array, und man kann sie sinnvoll aufrufen, ohne ihren Rückgabewert zu verwenden.
„Ein deutliches Anzeichen dafür, dass eine Funktion unrein ist, ist, wenn es sinnvoll ist, sie aufzurufen, ohne ihren Rückgabewert zu verwenden. Bei reinen Funktionen ist das ein No-Op.“ – Eric Elliott
Ein weiteres Konzept, das Sie kennen sollten, ist Idempotenz.
Idempotenz ist eine Eigenschaft bestimmter Operationen, bei denen das Ergebnis nach der ersten Ausführung dasselbe bleibt, egal wie oft man die Operation durchführt. Zum Beispiel ist das Setzen eines Wertes auf 5 idempotent, denn egal wie oft man es tut, der Wert bleibt 5.
let number = 5;
number = 5; // still 5
number = 5; // no change, still 5Alle reinen Funktionen sind idempotent, aber nicht alle idempotenten Funktionen sind rein. Eine idempotente Funktion kann idempotente Nebeneffekte verursachen. Eine reine Funktion kann das nicht. Das Löschen eines Datensatzes in einer Datenbank anhand der ID ist idempotent, da die Zeile der Tabelle nach weiteren Aufrufen gelöscht bleibt. Zusätzliche Aufrufe bewirken nichts.
Hier ist ein synchrones Beispiel.
const uniqueItems = new Set();
// This custom addItem function is idempotent because ...
function addItem(item) {
uniqueItems.add(item);
console.log(`Added item: ${item}`);
return uniqueItems.size;
}
addItem("apple"); // Outputs "Added item: apple", returns 1
// ... calling addItem with the same item twice leaves the set unchanged.
addItem("apple"); // Outputs "Added item: apple", returns 1
addItem("banana"); // Outputs "Added item: banana", returns 2
Idempotente Funktionen ohne Nebeneffekte besitzen eine Eigenschaft, die als referentielle Transparenz bekannt ist.
Das bedeutet: Wenn Sie einen Funktionsaufruf haben:
const result = square(7);Sie könnten diesen Funktionsaufruf durch das Ergebnis von square(7) ohne die Bedeutung des Programms zu ändern. Wenn zum Beispiel das Ergebnis von square(7) ist 49. Daher könnten Sie den obigen Code ändern zu:
const result = 49;und Ihr Programm würde immer noch genauso funktionieren.
Eine Sprache benötigt drei Funktionen, um funktionale Programmierung zu unterstützen, und JavaScript besitzt alle drei:
In JavaScript werden Funktionen als erstklassige Bürger behandelt. Das bedeutet, dass Funktionen in Variablen gespeichert werden können. Daher können Sie Funktionen verwenden als:
// Storing a function in a variable.
const greet = function() {
return "Hello, World!";
}
// Passing a function as an argument (callback).
function shout(fn) {
const message = fn();
console.log(message.toUpperCase());
}
shout(greet); // Output: "HELLO, WORLD!"
// Returning a function from another function.
function multiply(multiplicand) {
return function (multiplier) {
return multiplicand * multiplier;
}
}
const double = multiply(2);
console.log(double(5)); // Output: 10Wenn Sie erfahren möchten, was Funktionen erster Klasse in React ermöglichen, lesen Sie diesen Artikel über Higher-Order Components; der Link ist unten in der Beschreibung.
Das letzte Beispiel multiply die Sie gesehen haben, wird als „Higher-Order-Funktion“ bezeichnet, weil sie, wenn Sie sie zum ersten Mal mit einer Zahl aufrufen, eine Funktion zurückgibt.
Jede Funktion, die eine Funktion als Argument erhält oder eine Funktion zurückgibt, wird als Higher-Order-Funktion bezeichnet.
function greet() {
return "Hello World!"
}
const identity = x => x; // Same as myFunction from earlier ❗
identity(greet)(); // "Hello World!"
ClosureDie multiply Funktion von vorhin verbarg ein weiteres Schlüsselkonzept: Closure.
Eine Closure entsteht, wenn eine Funktion mit ihrem lexikalischen Geltungsbereich gebündelt wird. Mit anderen Worten, eine Closure ermöglicht den Zugriff auf den Geltungsbereich einer äußeren Funktion von einer inneren Funktion aus. Closures in JavaScript werden immer dann erstellt, wenn eine Funktion erzeugt wird, zum Zeitpunkt der Funktionserstellung.
Um eine Closure zu verwenden, definieren Sie eine Funktion innerhalb einer anderen Funktion und machen Sie sie zugänglich, indem Sie die innere Funktion zurückgeben oder an eine andere Funktion übergeben. Die innere Funktion kann auf Variablen im Geltungsbereich der äußeren Funktion zugreifen, selbst nachdem die äußere Funktion zurückgegeben wurde.
function multiply(multiplicand) {
return function(multiplier) {
return multiplicand * multiplier;
}
}
// Multiplicand of 2 gets captured in the closure because the inner
// returned function has access to it, even though the outer `multiply`
// function already ran to completion.
const double = multiply(2);
console.log(double(5)); // Output: 10Closures dienen drei Zwecken:
Punkt 2.) und 3.) werden Sie später in diesem Artikel sehen, also werfen wir einen Blick auf das Konzept der Datenprivatsphäre.
const createCounter = () => {
let count = 0;
return function increment() {
count = count + 1;
return count;
}
}
const counter1 = createCounter();
const counter2 = createCounter();
counter1(); // 1
counter1(); // 2
counter2(); // 1
let capturedCount = counter1(); // 3
capturedCount = capturedCount + 39; // 42
counter1(); // 4Kein externer Einfluss kann den count Wert von counter1. Sie müssen aufrufen counter1 um es zu inkrementieren.
Das ist wichtig, weil einige Anwendungen einen privaten Zustand erfordern. Ein gängiges Muster ist es, den privaten Schlüssel mit __.
const user = {
__secretKey: 'abcj'
}Allerdings wissen Junior-Entwickler möglicherweise nicht, dass __ signalisiert: „Diesen Schlüssel NICHT ändern.“, und mutieren ihn daher. Und erfahrene Entwickler denken manchmal, es sei in Ordnung, ihn zu ändern, weil sie glauben, es besser zu wissen.
Closures bieten eine zuverlässige Möglichkeit, die Datenprivatsphäre für alle durchzusetzen.
Wie Sie zu Beginn dieses Artikels gelernt haben, ist funktionale Programmierung deklarativ. Aber was bedeutet das?
Imperativ Code beschreibt „wie“ man Dinge tut. Der Code enthält die spezifischen Schritte, die erforderlich sind, um das gewünschte Ergebnis zu erzielen.
Deklarativ Code beschreibt „was“ zu tun ist. Das „Wie“ wird abstrahiert. Mit anderen Worten, bei der imperativen Programmierung geht es darum, den Prozess zur Erzielung eines Ergebnisses zu definieren. Dies wird als Kontrollfluss, bei dem Sie jeden Schritt der Berechnung vorgeben.
Deklarative Programmierung hingegen konzentriert sich auf die Definition des Ergebnisses, bekannt als Datenfluss. Hier beschreiben Sie, was Sie möchten, und das System bestimmt, wie es erreicht wird.
Hier ist ein Beispiel für imperativen Code.
let numbers = [1, 2, 3, 4, 5];
let doubled = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}
console.log(doubled); // Output: [2, 4, 6, 8, 10]
Und hier ist ein weiteres Beispiel für imperativen Code, diesmal jedoch mit einer benutzerdefinierten Funktion.
function filterEvens(numbers) {
let index = 0;
while (index < numbers.length) {
if (numbers[index] % 2 !== 0) {
// Removes the item at the current index if it's odd.
numbers.splice(index, 1);
} else {
// Only move to the next index if the current item was not removed
// because the current index gets taken by the value after the
// deleted one.
index++;
}
}
}
let numbers = [1, 2, 3, 4, 5];
filterEvens(numbers);
console.log(numbers); // Output: [2, 4Bevor Sie sich deklarativen Code ansehen, müssen Sie Unveränderlichkeit und Abstraktion verstehen.
Unveränderlichkeit in der Programmierung bedeutet, dass ein Objekt oder Wert nicht verändert werden kann, nachdem es erstellt wurde; stattdessen führen alle Änderungen zu einem neuen Objekt oder Wert.
const originalArray = [1, 2, 3];
// Creates a new array by spreading the original and adding a new element.
const newArray = [...originalArray, 4];
console.log(originalArray); // Output: [1, 2, 3]
console.log(newArray); // Output: [1, 2, 3, 4]Ebenso ist mutabler Zustand ein Zustand, der verändert werden kann nachdem es erstellt wurde, geändert werden.
const array = [1, 2, 3];
array.push(4); // Modifies the original array by adding a new element.
console.log(array); // Output: [1, 2, 3, 4]Unveränderlichkeit ist ein zentrales Konzept der funktionalen Programmierung, da der Datenfluss in Ihrem Programm dadurch erhalten bleibt. Die Historie des Zustands wird beibehalten, und es hilft, das Einschleichen seltsamer Fehler in Ihre Software zu verhindern.
„Nicht-Determinismus = parallele Verarbeitung + veränderlicher Zustand“ – Martin Odersky
Sie möchten Determinismus, um Ihre Programme leicht nachvollziehbar zu machen, und parallele Verarbeitung, um Ihre Anwendungen leistungsfähig zu halten. Daher müssen Sie auf veränderlichen Zustand verzichten.
Im Allgemeinen wird Ihr Code, wenn Sie ihn mit funktionaler Programmierung schreiben, deterministischer, leichter nachvollziehbar, einfacher zu warten, modularer und besser testbar.
Abstraktion ist ein grundlegendes Konzept in der Programmierung, das darin besteht, die komplexe Realität zu verbergen und gleichzeitig nur die notwendigen Teile eines Objekts oder Systems freizulegen.
Es gibt zwei Arten von Abstraktion: Generalisierung und Spezialisierung.
Generalisierung ist, wenn Sie eine universellere Form von etwas für die Verwendung in mehreren Kontexten erstellen. Dieser Prozess identifiziert gemeinsame Merkmale zwischen verschiedenen Komponenten und entwickelt ein einziges Modell, um alle Variationen darzustellen.
Das ist es, woran die meisten Leute denken, wenn sie „Abstraktion“ hören, und worauf sich Eric Elliott bezieht, wenn er sagt:
„Junior-Entwickler glauben, sie müssten viel Code schreiben, um viel Wert zu schaffen.Senior-Entwickler verstehen den Wert des Codes, den niemand schreiben musste.“ – Eric Elliott
Spezialisierung ist, wenn Sie die Abstraktion auf einen spezifischen Anwendungsfall anwenden und hinzufügen, was die aktuelle Situation unterscheidet.
Das Schwierige ist zu wissen, wann man verallgemeinern und wann man spezialisieren sollte. Leider gibt es dafür keine Faustregel – man entwickelt ein Gefühl für beides mit Erfahrung.
Und Sie werden feststellen: Abstraktion ist der Schlüssel zur Einfachheit.
„Einfachheit bedeutet, das Offensichtliche wegzulassen und das Sinnvolle hinzuzufügen.“ - John Maeda
Mit dem Paradigma der funktionalen Programmierung können Sie die schönsten Abstraktionen erstellen.
An diesem Punkt dürsten Sie wahrscheinlich nach Beispielen, und Sie werden bald welche sehen.
Nun ist es an der Zeit, sich deklarativen Code anzusehen, der Ihnen auch mehr Unveränderlichkeit und etwas Abstraktion zeigen wird.
Denken Sie daran: Bei deklarativer Programmierung geht es darum, „was“ zu tun ist.
Das perfekte Beispiel für deklarativen Code in JavaScript sind die nativen Array-Methoden. Sie werden die drei gängigsten sehen: map, filter und reduce.
map
Werfen Sie einen Blick auf die map Funktion zuerst. Sie macht genau das, was wir zuvor mit der for -Schleife getan haben, aber der Code ist viel prägnanter.
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2); // Output: [2, 4, 6, 8, 10]
Die map -Methode ist ein perfektes Beispiel für Abstraktion.
map entfernt das Offensichtliche: das Iterieren über das Array und das Ändern jedes Wertes. Da sie eine Funktion entgegennimmt, ist die map -Methode eine Higher-Order-Funktion.
Sie liefern nur das Wesentliche: die Funktion, die eine Zahl verdoppelt, welche map auf jede Zahl im Array anwendet. Diese double -Funktion ist eine anonyme Funktion, die die prägnante Lambda-Syntax verwendet.
Im Allgemeinen ist eine prägnante Lambda eine vereinfachte Art, eine Funktion mit minimaler Syntax zu schreiben. In JavaScript bezieht sie sich auf die Pfeilfunktionssyntax.
map ist ebenfalls unveränderlich, da es ein neues Array zurückgibt. numbers und doubled sind zwei unterschiedliche Arrays, und das numbers Array enthält immer noch die Zahlen 1 bis 5. Sie können dies überprüfen, indem Sie mit der Identitäts- Funktion abbilden, die ihren Input zurückgibt.
const numbers = [1, 2, 3, 4, 5];
const clone = numbers.map(x => x);
console.log(numbers === clone); // falseObwohl numbers und clone beide ein Array mit den Zahlen 1 bis 5 sind, handelt es sich um unterschiedliche Array-Instanzen.
Sie fragen sich vielleicht: "Erzeugt das nicht Unmengen an Daten, die niemand nutzt?" Nun, irgendwie schon, aber im Vergleich zu dem, wozu moderne Laptops fähig sind, ist die Datenmenge winzig, und JavaScript verfügt über eine Garbage Collection, die den veralteten Speicher bereinigt.
filterconst fruits = ['apple', 'banana', 'citrus'];
const containsA = fruits.filter(fruit => fruit.includes('a'));
// Output: ['apple', 'banana'];Die filter Methode nimmt eine spezielle Funktion namens "Prädikat" entgegen. Ein Prädikat ist eine Funktion, die immer nur einen booleschen Wert zurückgibt. Es testet jedes Element im Array. Wenn es zurückgibt true, wird das Element in das resultierende Array aufgenommen.
filter gibt ebenfalls ein neues Array zurück, d.h. es ist unveränderlich.
reduceDie reduce -Methode in JavaScript verarbeitet ein Array, um einen einzelnen Ausgabewert zu erzeugen. Sie nimmt eine Reducer-Funktion und einen optionalen Startwert entgegen. Die Reducer-Funktion selbst akzeptiert zwei Parameter: einen Akkumulator (der das akkumulierte Ergebnis enthält) und den currentValue aus dem Array. Die reduce -Methode ist ebenfalls unveränderlich.
Hier ist ein Beispiel, in dem Sie reduce auf einem Array von Zahlen von 1 bis 4 verwenden können, um diese zu summieren.
const numbers = [1, 2, 3, 4];
const sumReducer = (accumulator, currentValue) => accumulator +
currentValue;
const total = numbers.reduce(sumReducer, 0); console.log(total); //
Output: 10In diesem Beispiel, reduce wird auf dem numbers -Array aufgerufen. Die sumReducer -Funktion wird verwendet, um jede Zahl zu einer laufenden Summe zu addieren, beginnend bei 0. Hier ist, was bei jedem Schritt passiert:
Am Ende des Prozesses gibt die reduce -Methode 10 zurück, was die Summe aller Elemente im Array ist. Dies zeigt, wie reduce verwendet werden kann, um ein Array durch wiederholte Anwendung einer Funktion in einen einzelnen Wert umzuwandeln.
reduce ist die mächtigste Methode, da Sie map und filter mit reduceimplementieren können, aber keines von beiden filter noch reduce mit map und weder map noch reduce mit filter.
Sie können implementieren map mit reduce so:
const mapUsingReduce = (array, mapFunction) =>
array.reduce(
(accumulator, current) => [...accumulator, mapFunction(current)],
[],
);
const numbers = [1, 2, 3, 4];
const doubled = mapUsingReduce(numbers, x => x * 2);
console.log(doubled); // Output: [2, 4, 6, 8]
Sie können implementieren filter mit reduce so:
const filterUsingReduce = (array, filterFunction) =>
array.reduce(
(accumulator, current) =>
filterFunction(current) ? [...accumulator, current] : accumulator,
[],
);
const numbers = [1, 2, 3, 4];
const evens = filterUsingReduce(numbers, x => x % 2 === 0);
console.log(evens); // Output [2, 4]
In der funktionalen Programmierung finden Sie viele Ausdrücke und wenige Anweisungen. Ausdrücke vermeiden Zwischenvariablen, während Anweisungen oft Nebenwirkungen und veränderlichen Zustand mit sich bringen.
Imperativer Code verwendet häufig Anweisungen. Eine Anweisung ist ein Codeabschnitt, der eine Aktion ausführt.
for, while, usw.// Loops:
// A for loop that logs numbers 0 to 4.
for (let i = 0; i < 5; i++) {
console.log(i);
}
// A while loop that decrements x and logs it until x is no longer greater
than 0.
while (x > 0) {
x--;
console.log(x);
}if, switch, usw.// An if...else statement that logs if x is positive or not.
if (x > 0) {
console.log("x is positive");
} else {
console.log("x is zero or negative");
}
// A switch statement that handles different color cases.
switch (color) {
case "red":
console.log("Color is red");
break;
case "blue":
console.log("Color is blue");
break;
default:
console.log("Color is not red or blue");
}try...catch, throw, usw.// A try...catch block that handles errors from riskyFunction.
try {
let result = riskyFunction();
} catch (error) {
console.error(error);
}
throw new Error("Something went wrong"); // Throws a new error with a
message.Abgesehen von Funktionen: Wenn es sich um ein Schlüsselwort mit geschweiften Klammern handelt, ist es wahrscheinlich eine Anweisung. (❗)
Deklarativer Code bevorzugt Ausdrücke. Ein Ausdruck ergibt einen Wert.
42; // The number 42 is a literal expression.
"Hello"; // The string "Hello" is a literal expression.5 + 3; // Evaluates to 8.
x * y; // Evaluates to the product of x and ytrue && false; // Evaluates to false.
x || y; // Evaluates to x if x is true, otherwise y.const funcExpr = function() { return 42; }; // Defines a function
expression.
const arrowFunc = () => 42; // Defines an arrow function expression.{ name: "John", age: 30 }; // Object initializer expression.
[1, 2, 3]; // Array initializer expression.obj.name; // Accesses the "name" property of obj.
array[0]; // Accesses the first element of array.square(7); // Evaluates to 49.
Math.max(4, 3, 2); // Evaluates to 4.
Du wirst gleich ein neues Verständnis von Code erlangen und Superkräfte gewinnen, also „klink dich ein“.
„Jede Softwareentwicklung ist Komposition: Der Akt, ein komplexes Problem in kleinere Teile zu zerlegen und diese kleineren Lösungen dann zusammenzusetzen, um Ihre Anwendung zu bilden.“ - Eric Elliot
Wann immer man Funktionen zusammen verwendet, „komponiert“ man sie.
const increment = n => n + 1;
const double = n => n * 2;
function doubleInc(n) {
const doubled = double(n);
const incremented = increment(doubled);
return incremented;
}
doubleInc(5); // 11Aber wie bei allem im Leben kann man es besser machen, wenn man es bewusst tut. Der obige Code ist tatsächlich NICHT die ideale Art, ihn zu schreiben, weil:
Je mehr Code man schreibt, desto größer ist die Angriffsfläche für Fehler.
weniger Code = weniger Angriffsfläche für Fehler = weniger Fehler
Die offensichtliche Ausnahme sind klare Benennung und Dokumentation. Es ist in Ordnung, einer Funktion einen längeren Namen zu geben und Docstrings bereitzustellen, um es Ihren Lesern zu erleichtern, Ihren Code zu verstehen.
Sie können die Angriffsfläche für Fehler reduzieren, indem Sie vermeiden, Zwischenergebnisse in Variablen zu speichern.
const increment = n => n + 1;
const double = n => n * 2;
const doubleInc = n => inc(double(n));
doubleInc(5); // 11In der Mathematik ist die Funktionskomposition die Verknüpfung zweier Funktionen f und g und die Anwendung einer Funktion auf das Ergebnis einer anderen Funktion: h(x) = (f ∘ g)(x) = f(g(x)). Hinweis: Der hohle Punkt ∘ wird als Kompositionsoperator bezeichnet. In mathematischer Notation, wenn Sie zwei Funktionen haben f und g, und deren Komposition als (f ∘ g)(x)geschrieben wird, bedeutet dies, dass Sie zuerst g auf x und dann anwenden f auf das Ergebnis von g(x). Für Ihr Beispiel, f(n) = n1 und g(n) = 2n, berechnet die Komposition h(n) = f(g(n)) den Wert 2n + 1.
Hinweis: Mathematiker verwenden im Allgemeinen x (oder y oder z usw.), um eine beliebige Variable darzustellen, aber im obigen Codebeispiel n wird verwendet, um subtil anzudeuten, dass der Parameter eine Zahl sein soll. Der unterschiedliche Name hat keine Auswirkung auf das Ergebnis.
Man kann den Kompositionsoperator abstrahieren ∘ in eine Funktion namens compose2 die zwei Funktionen entgegennimmt und diese in mathematischer Reihenfolge zusammensetzt.
const compose2 = (f, g) => x => f(g(x)); // ∘ operator
const increment = n => n + 1; // f(n)
const double = n => n * 2; // g(n)
const doubleInc = compose2(increment, double); // h(n)
doubleInc(5); // 11
compose2 funktioniert nur für jeweils zwei Funktionen.
Aber aufgepasst, denn hier wird es mächtig.
Wenn Sie eine beliebige Anzahl von Funktionen zusammensetzen möchten, können Sie eine verallgemeinerte compose.
const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);
// reduceRight works like reduce, but iterates the array from
// the last item to the first item.
const increment = n => n + 1;
const double = n => n * 2;
const square = n => n * n;
const incDoubleSquare = compose(square, double, increment);
incDoubleSquare(3); // 64Die compose Funktion ist hier mit den mathematischen Variablennamen geschrieben. Wenn Sie die Namen verwenden möchten, die Sie vielleicht von reduce gewohnt sind, dann würden Sie es so schreiben:
const compose =
(...functions) =>
initialValue =>
functions.reduceRight(
(accumulator, currentFunction) => currentFunction(accumulator),
initialValue,
);
const increment = n => n + 1;
const double = n => n * 2;
const square = n => n * n;
const incDoubleSquare = compose(square, double, increment);
incDoubleSquare(3); // 64compose nimmt mehrere Funktionen als Argumente entgegen und sammelt sie in einem Array fns mittels der Rest-Syntax. Sie gibt dann eine Kindfunktion zurück, die den initialValue entgegennimmt x und gibt das Array zurück fns mit derreduceRight Methode darauf angewendet. Die reduceRight Methode nimmt dann eine Callback-Funktion und den initialValue x.
Diese Callback-Funktion ist das Herzstück von compose. Sie nimmt den Akkumulator y (beginnend bei x) und den currentValue f, die eine Funktion aus dem Array ist fns. Sie gibt dann diese Funktion zurück f – aufgerufen mit dem Akkumulator y.
Das Ergebnis dieses Funktionsaufrufs wird dann zum Akkumulator y für die nächste Iteration. Hier wird die nächste Funktion f aus fns mit diesem neuen Akkumulatorwert aufgerufen. Dies wiederholt sich, bis der initialValue x durch alle Funktionen aus dem Array weitergegeben und transformiert wurde fns.
Viele Menschen, die es gewohnt sind, Texte von links nach rechts zu lesen, empfinden es als unintuitiv, Funktionen in mathematischer Reihenfolge zu komponieren. Viele funktionale Programmpakete bieten eine weitere Funktion, die üblicherweise pipe, die Funktionen von links nach rechts in umgekehrter mathematischer Reihenfolge zusammensetzt.
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const increment = n => n + 1;
const double = n => n * 2;
const square = n => n * n;
const incDoubleSquare = pipe(increment, double, square);
incDoubleSquare(3); // 64traceSie fragen sich vielleicht gerade: „Aber Moment mal, was ist, wenn Sie Ihren Code debuggen möchten? Dann müssen Sie die Zwischenergebnisse in Variablen erfassen, oder?“
Tatsächlich nicht. Sie benötigen lediglich eine Hilfsfunktion höherer Ordnung namens trace.
const trace = msg => x => {
console.log(msg, x);
return x;
}Und so können Sie sie verwenden.
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const trace = msg => x => {
console.log(msg, x);
return x;
}
const increment = n => n + 1;
const double = n => n * 2;
const square = n => n * n;
const incDoubleSquare = pipe(
increment,
trace('before double'),
double,
trace('after double'),
square
);
incDoubleSquare(3); // 64
// Also logs out:
// before double 4
// after double 8
Sie haben eine weitere Technik kennengelernt, die als Currying bezeichnet wird.
Currying ist eine Transformation von Funktionen, die eine Funktion von aufrufbar als f(a, b, c) in aufrufbar als f(a)(b)(c). Mit anderen Worten, ist eine Funktion dann gecurried, wenn sie jeden ihrer Parameter einzeln entgegennehmen kann.
function addUncurried(a, b) {
return a + b;
}
function addCurried(a) {
return function(b) {
return a + b;
}
}
addUncurried(41, 1); // 42
addCurried(41)(1); // 42Mit Arrow-Funktionen können diese Definitionen zu Einzeilern werden, indem man ihre impliziten Rückgabewerte nutzt.
const addUncurried = (a, b) => a + b;
const addCurried = a => b => a + b;
addUncurried(41, 1); // 42
addCurried(41)(1); // 42Hier ist eine addCurried eine Funktion, die eine Zahl a entgegennimmt und eine Funktion zurückgibt, nämlich b => a + b. Sie können es so lesen:
const addCurried = (a => (b => a + b));Sie können jede Funktion currying. Zum Beispiel können Sie benutzerdefinierte map und Reduce-Funktionen.
const map = fn => arr => arr.map(fn);
const reduce = fn => x => arr => arr.reduce(fn, x);Map akzeptiert zwei Parameter und Reduce akzeptiert drei. Die Anzahl der Parameter, die eine Funktion in ihrer Definition erwartet, wird als Arität.
Es gibt Kurzbezeichnungen für Funktionen, die 1, 2 und 3 Parameter akzeptieren.
square.map.reduce.Im Kontext des Currying ist das Verständnis der Arität wichtig, da jeder Schritt in einer gecurryten Funktion die Arität um eins reduziert, bis alle erwarteten Argumente empfangen wurden und die endgültige Operation ausgeführt werden kann.
Außerdem kann die erste Funktion einer Komposition eine beliebige Arität haben, aber jede folgende Funktion muss unär sein.
const add3 = (a, b, c) => a + b + ; // Ternary
const double = n => n * 2; // Unary
const addThenDouble = pipe(add3, double);
addThenDouble(6, 7, 8); // 42Übung: Erstellen Sie Ihre eigene benutzerdefinierte filter Funktion, die curried ist und ein Prädikat entgegennimmt pred und dann ein Array arr und dann das Array basierend auf dem Prädikat filtert.
Wäre es nicht schön, wenn man eine Funktion hätte, die jede beliebige Funktion currieren kann?
const addUncurried = (a, b) => a + b;
const curry = /* ... magic? ... */
const addCurried = curry(addUncurried);Nun, hier ist sie.
const addUncurried = (a, b) => a + b;
const curry = (f, array = []) =>
(...args) =>
(a => (a.length >= f.length ? f(...a) : curry(f, a)))([
...array,
...args,
]);
// NOTE: because of f.length, this implementation of `curry` fails with
// functions that use default parameters.
const addCurried = curry(addUncurried);
const increment = addCurried(1);
increment(4); // 5
addCurried(1, 4); // 5
addCurried(1)(4); // 5Das vorherige Beispiel verwendete die mathematischen Namen für die Variablen. Wenn Sie die Variablen aussagekräftiger benennen möchten, um curry jetzt besser zu verstehen, können Sie die Funktion so schreiben:
const curry =
(targetFunction, collectedArguments = []) =>
(...currentArguments) =>
(allArguments =>
allArguments.length >= targetFunction.length
? targetFunction(...allArguments)
: curry(targetFunction, allArguments))([
...collectedArguments,
...currentArguments,
]);curry verwendet Rekursion, was bedeutet, dass eine Funktion sich selbst zu Iterationszwecken aufruft. Lassen Sie uns das aufschlüsseln:
f oder targetFunction: Die ursprüngliche Funktion, die Sie currieren möchten.array oder collectedArguments: Ein Array zum Sammeln der Argumente aller Currying-Aufrufe. In jedem Zyklus (außer dem letzten) werden neue Argumente hinzugefügt.args oder currentArguments: Ein Array der Argumente, die beim aktuellen Aufruf der gecurryten Funktion (in diesem Fall: addCurried) entgegengenommen werden.a oder allArguments: Ein Array von Argumenten, das aus den currentArguments und den collectedArguments aus den vorherigen Aufrufen zusammengeführt wird.Wenn wir addCurried mit einem oder mehreren Argumenten aufrufen, werden diese aktuellen Argumente als args in der Kindfunktion von curry. Diese Kindfunktion gibt eine weitere Funktion zurück (Enkel-Funktion von curry).
Die Enkel-Funktion ruft sich sofort selbst auf mit einem Array, das aus den gesammelten Argumenten array und den aktuellen Argumenten args konkateniert wurde, wobei sie diese Werte von ihrer Großeltern- und Eltern-Closure erhält. Sie nimmt dieses neue Array als die gesamten Argumente a und prüft, ob die gesamten Argumente a die gleiche oder eine höhere Anzahl von Argumenten enthalten als die Zielfunktion f deklariert ist.
Wenn ja, bedeutet dies, dass alle notwendigen Currying-Aufrufe durchgeführt wurden: Sie gibt einen abschließenden Aufruf der Zielfunktion f zurück, aufgerufen mit den gesamten Argumenten a. Wenn nicht, ruft sie curry rekursiv erneut auf mit der Zielfunktion f und den neuen gesamten Argumenten a, die dann als das neue Array der gesammelten Argumente übernommen werden. Dies wiederholt sich, bis die a.length Bedingung erfüllt ist.
Hinweise:
a und die aktuellen Argumente args denselben Wert haben, da dem gesammelten Argument-Array noch nichts hinzugefügt wurde.Wie Sie bereits gelernt haben, findet eine Anwendung statt, wenn die Argumente verwendet werden, um die Parameter der Funktion zu ersetzen. Dies ermöglicht es der Funktion, ihre Aufgabe mit den bereitgestellten Argumenten auszuführen.
const add = (a, b) => a + b;
const inc = n => add(1, n);Eine partielle Anwendung ist der Prozess, eine Funktion auf einige, aber nicht alle, ihrer Argumente anzuwenden. Dadurch wird eine neue Funktion erstellt, die Sie zur späteren Verwendung in einer Variablen speichern können. Die neue Funktion benötigt weniger Argumente zur Ausführung, da sie nur die verbleibenden Parameter als Argumente entgegennimmt.
Partielle Anwendungen sind nützlich für die Spezialisierung, wenn Sie eine Funktion mit gemeinsamen Parametern wiederverwenden möchten.
const add = a => b => a + b;
const inc = add(1); // point-free
const incPointed = n => add(1)(n); // pointedPoint-Free-Stil
inc ist im Point-Free-Stil definiert, was bedeutet, dass Sie eine Funktion schreiben, ohne die Parameter zu erwähnen.
inc verwendet Closure, weil das Argument 1 in der Closure von add als ein.Es gibt zwei Anforderungen, damit Ihr Code zusammensetzbar ist: "data last" und die Daten müssen übereinstimmen.
"data last" bedeutet, dass die Daten, mit denen Ihre Funktionen arbeiten, ihr letzter Parameter sein sollten. Für die add und multiply Funktionen, die Sie zuvor gesehen haben, ist die Reihenfolge der Argumente irrelevant, da sie kommutativ sind.
const add = a => b => a + b;
const otherAdd = b => a => a + b;
const a = 41;
const b = 1;
console.log(add(41, 1) === otherAdd(41, 1)); // trueAber die Division ist es NICHT, daher werden Sie die Bedeutung des "data last"-Prinzips anhand einer divide Funktion kennenlernen.
const divideDataLast = (y) => (x) => x / y;
const divideDataFirst = (x) => (y) => x / y;Nehmen wir an, Sie möchten sich spezialisieren und eine halve Funktion erstellen, die eine Zahl entgegennimmt und diese durch zwei teilt.
Mit der "data first"-Funktion ist es Ihnen unmöglich, eine halve Funktion im Point-Free-Stil zu definieren.
const divideDataLast = (y) => (x) => x / y;
const divideDataFirst = (x) => (y) => x / y;
const halve = divideDataLast(2);
// const halveFail = divideDataFirst(2); 🚫 fails
const halvePointed = (n) => divideDataFirst(n)(2);halfFail erfasst 2 im Closure von divideDataFirst. Es ist eine Funktion, die eine Zahl entgegennimmt und bei Aufruf 2 durch diese übergebene Zahl teilt.
Grundsätzlich müssen Sie Ihre Funktionen nach dem „Data Last“-Prinzip schreiben, um partielle Anwendung zu ermöglichen.
Übrigens ist die beste Bibliothek für funktionale Programmierung im „Data Last“-Paradigma Ramda. Hier ist ein Ausblick, was Sie mit Ramda tun können. Sie können dies zu einem späteren Zeitpunkt eingehend verstehen.
import { assoc, curry, keys, length, pipe, reduce, values } from 'ramda';
const size = pipe(values, length);
size({ name: 'Bob', age: 42 }); // 2
size(['a', 'b', 'c', 'd']); // 4
const renameKeys = curry((keyReplacements, object) =>
reduce(
(accumulator, key) =>
assoc(keyReplacements[key] || key, object[key], accumulator),
{},
keys(object),
),
);
const input = { firstName: 'Elisia', age: 22, type: 'human' };
const keyReplacements = { firstName: 'name', type: 'kind', foo: 'bar' };
renameKeys(keyReplacements)(input);
// Output: { name: 'Elisia', age: 22, kind: 'human' }Ähnlich können Sie Funktionen nur mit „Data Last“ effektiv zusammensetzen, da die Typen der Argumente und Rückgabewerte von Funktionen übereinstimmen müssen, um sie zu komponieren. Zum Beispiel können Sie eine Funktion, die ein Objekt akzeptiert und einen String zurückgibt, nicht mit einer Funktion zusammensetzen, die ein Array empfängt und eine Zahl zurückgibt.
// (number, number) => number[]
const echo = (value, times) => Array(times).fill(value);
// number[] => number[]
const doubleMap = array => array.map(x => x * 2);
// Correct composition. ✅
const echoAndDoubleMap = compose(doubleMap, echo);
// Reminder, the first function in a composition does NOT need to
// be unary, and echo is binary.
// echoAndDoubleMap starts binary and ends unary.
console.log(echoAndDoubleMap(3, 4)); // [6, 6, 6, 6]
// Incorrect composition that will throw an error. ❌
const wrongOrder = compose(echo, doubleMap);
try {
// This will fail because doubleMap expects an array,
// instead of two numbers.
console.log(wrongOrder(3, 4));
} catch (error) {
console.error("Error:", error.message); // Error: array.map is not a
function
}
Jetzt sind Sie dran. Versuchen Sie die folgende Übung, um das Gelernte aus diesem Artikel zu festigen. Wenn Sie nicht weiterkommen, können Sie jederzeit nach oben scrollen und es erneut lesen. Wenn Sie die Übung nicht machen möchten, lesen Sie einfach die Lösung und folgen Sie den Schritten.
Erstellen Sie eine Funktion, die ein Array von Zahlen entgegennimmt und alle geraden Zahlen filtert (also die ungeraden Zahlen verwirft), dann alle geraden Zahlen verdoppelt und schließlich das Ergebnis summiert. Zerlegen Sie Ihre Funktionen in ihre grundlegendsten Abstraktionen und setzen Sie sie dann punktfrei zusammen. (Tipp: Sie müssen den Modulo-Operator nachschlagen % um zu prüfen, ob eine Zahl gerade ist.)
const curry =
(f, array = []) =>
(...args) =>
(a => (a.length >= f.length ? f(...a) : curry(f, a)))([
...array,
...args,
]);
const add = curry((a, b) => a + b);
const multiply = a => b => a * b;
const inc = add(1);
const double = multiply(2);
const isEven = n => n % 2 === 0;
const map = fn => arr => arr.map(fn);
const filter = pred => arr => arr.filter(pred);
const reduce = curry((fn, acc, arr) => arr.reduce(fn, acc));
const doubleMap = map(double);
const filterEvens = filter(isEven);
const sum = reduce(add, 0);
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const mozart = pipe(filterEvens, doubleMap, sum);
mozart([1, 2, 3, 4, 5]); // 12Abschließend, ist das komplizierter, als einfach Folgendes zu schreiben?
const mozart = numbers =>
numbers
.filter(n => n % 2 === 0)
.map(n => n * 2)
.reduce((a, b) => a + b, 0);
mozart([1, 2, 3, 4, 5]); // 12Ja, absolut! Sie sollten immer die einfachste Implementierung für Ihre Anforderungen verwenden, auch bekannt als KISS (Keep It Simple, Stupid) oder YAGNI (You Ain't Going To Need It).
Funktionale Programmierung glänzt, wenn Ihre Anwendung wächst und Sie Ihren Code verallgemeinern und spezialisieren müssen, damit er gut skaliert und wartbar bleibt. Auf diese Weise ist Ihr Code modularer und viel einfacher zu testen, wiederzuverwenden und zu refaktorisieren. Zukünftige Artikel werden Ihnen reale Beispiele zeigen, wie Sie diese Techniken anwenden können.
Sie kennen nun die 20 % der funktionalen Programmierung, die Ihnen 80 % des Ergebnisses liefern.