22 Funktionale Programmierung in JavaScript

Funktionale Programmierung (FP) ist ein Programmierparadigma, das die Anwendung von Funktionen betont und dabei versucht, Seiteneffekte zu vermeiden und veränderliche Daten zu minimieren. JavaScript unterstützt funktionale Programmierkonzepte und bietet vielseitige Möglichkeiten, diese Prinzipien in der Praxis anzuwenden. In diesem Abschnitt werden wir die Grundlagen der funktionalen Programmierung in JavaScript, ihre Konzepte und praktischen Anwendungen untersuchen.

22.1 Grundprinzipien der funktionalen Programmierung

Die funktionale Programmierung basiert auf verschiedenen Schlüsselkonzepten:

22.1.1 Pure Funktionen

Eine pure Funktion ist eine Funktion, die: - Bei gleichen Eingaben immer die gleichen Ausgaben liefert - Keine Seiteneffekte hat (keine Änderung von externen Zuständen) - Nicht auf externe Zustände angewiesen ist

// Impure Funktion (mit Seiteneffekt)
let counter = 0;
function incrementCounter() {
  counter++;
  return counter;
}

// Pure Funktion
function add(a, b) {
  return a + b;
}

22.1.2 Unveränderlichkeit (Immutability)

In der funktionalen Programmierung werden Daten als unveränderlich betrachtet. Statt Daten zu ändern, erzeugen wir neue Daten mit den gewünschten Änderungen.

// Veränderlicher Ansatz (nicht funktional)
const zahlen = [1, 2, 3, 4, 5];
zahlen.push(6); // Verändert das Original-Array

// Unveränderlicher Ansatz (funktional)
const originalZahlen = [1, 2, 3, 4, 5];
const neueZahlen = [...originalZahlen, 6]; // Erstellt ein neues Array

22.1.3 Funktionen höherer Ordnung

Funktionen höherer Ordnung sind Funktionen, die: - Andere Funktionen als Argumente annehmen können - Funktionen als Ergebnis zurückgeben können

// Funktion, die eine Funktion als Argument nimmt
function mapNumbers(numbers, transformFn) {
  return numbers.map(transformFn);
}

// Verwendung
const double = n => n * 2;
const doubled = mapNumbers([1, 2, 3], double); // [2, 4, 6]

// Funktion, die eine Funktion zurückgibt
function multiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

const triple = multiplier(3);
const tripled = triple(4); // 12

22.1.4 Deklarativer vs. imperativer Stil

Funktionale Programmierung bevorzugt einen deklarativen Stil (beschreibt, “was” erreicht werden soll) gegenüber einem imperativen Stil (beschreibt, “wie” etwas erreicht werden soll).

// Imperativ
const zahlen = [1, 2, 3, 4, 5];
const geradeZahlen = [];
for (let i = 0; i < zahlen.length; i++) {
  if (zahlen[i] % 2 === 0) {
    geradeZahlen.push(zahlen[i]);
  }
}

// Deklarativ
const zahlen = [1, 2, 3, 4, 5];
const geradeZahlen = zahlen.filter(zahl => zahl % 2 === 0);

22.2 Funktionale Programmierkonzepte in JavaScript

22.2.1 First-Class Functions

In JavaScript sind Funktionen “First-Class Citizens”, was bedeutet, dass sie wie jeder andere Datentyp behandelt werden können:

// Funktionen können Variablen zugewiesen werden
const greet = function(name) {
  return `Hallo, ${name}!`;
};

// Funktionen können in Datenstrukturen gespeichert werden
const actions = [
  function add(a, b) { return a + b; },
  function subtract(a, b) { return a - b; }
];

// Funktionen können an andere Funktionen übergeben werden
function executeOperation(operation, a, b) {
  return operation(a, b);
}
const result = executeOperation((a, b) => a * b, 5, 3); // 15

22.2.2 Closures

Closures sind ein wesentlicher Bestandteil der funktionalen Programmierung in JavaScript. Eine Closure entsteht, wenn eine Funktion auf Variablen aus ihrem äußeren Lexikalischen Scope zugreifen kann, auch nachdem dieser äußere Scope nicht mehr existiert.

function createCounter(initialValue = 0) {
  // Die Variable count ist nur innerhalb von createCounter sichtbar
  let count = initialValue;
  
  // Diese zurückgegebene Funktion hat Zugriff auf count, auch nachdem createCounter beendet wurde
  return function() {
    return count++;
  };
}

const counter = createCounter(10);
console.log(counter()); // 10
console.log(counter()); // 11
console.log(counter()); // 12

Closures ermöglichen: - Kapselung von Daten - Erzeugung von Funktionsfabriken - Implementation von privaten Variablen

22.2.3 Funktionskomposition

Die Funktionskomposition ist eine Technik, um komplexe Funktionen aus einfacheren Funktionen zu erstellen.

// Einfache Funktionen
const add10 = x => x + 10;
const multiply2 = x => x * 2;
const subtract5 = x => x - 5;

// Manuelle Komposition
const transformValue = x => subtract5(multiply2(add10(x)));
console.log(transformValue(5)); // ((5 + 10) * 2) - 5 = 25

// Hilfsfunktion zur Funktionskomposition
function compose(...functions) {
  return function(initialValue) {
    return functions.reduceRight((value, fn) => fn(value), initialValue);
  };
}

const transform = compose(subtract5, multiply2, add10);
console.log(transform(5)); // ((5 + 10) * 2) - 5 = 25

22.2.4 Pipe

Pipe ist ähnlich wie Compose, aber die Funktionen werden in der Reihenfolge ihrer Lesbarkeit (von links nach rechts) angewendet:

function pipe(...functions) {
  return function(initialValue) {
    return functions.reduce((value, fn) => fn(value), initialValue);
  };
}

const transform = pipe(add10, multiply2, subtract5);
console.log(transform(5)); // ((5 + 10) * 2) - 5 = 25

22.2.5 Currying

Currying ist die Technik, eine Funktion mit mehreren Argumenten in eine Sequenz von Funktionen umzuwandeln, die jeweils nur ein Argument annehmen.

// Normale Funktion mit mehreren Parametern
function add(a, b, c) {
  return a + b + c;
}

// Gecurryte Version
function curriedAdd(a) {
  return function(b) {
    return function(c) {
      return a + b + c;
    };
  };
}

// Verwendung
console.log(add(1, 2, 3)); // 6
console.log(curriedAdd(1)(2)(3)); // 6

// Mit Arrow Functions wird es kompakter
const curriedAddArrow = a => b => c => a + b + c;
console.log(curriedAddArrow(1)(2)(3)); // 6

// Hilfsfunktion zum Currying
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
}

const curriedAddFlex = curry(add);
console.log(curriedAddFlex(1)(2)(3)); // 6
console.log(curriedAddFlex(1, 2)(3)); // 6
console.log(curriedAddFlex(1)(2, 3)); // 6

Currying bietet Vorteile wie: - Partielle Anwendung von Funktionen - Erzeugung spezialisierter Funktionen - Verbesserte Wiederverwendbarkeit

22.2.6 Partielle Anwendung

Partielle Anwendung ist das Vorab-Binden einiger Argumente einer Funktion, um eine neue Funktion zu erzeugen:

function partial(fn, ...presetArgs) {
  return function(...laterArgs) {
    return fn(...presetArgs, ...laterArgs);
  };
}

function greet(greeting, name) {
  return `${greeting}, ${name}!`;
}

const sayHello = partial(greet, "Hallo");
console.log(sayHello("Max")); // "Hallo, Max!"

22.3 Funktionale Arrays und Objekte in JavaScript

JavaScript bietet verschiedene integrierte Methoden, die funktionale Programmierung mit Arrays und Objekte erleichtern.

22.3.1 Array-Methoden

const zahlen = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// map: Transformiert jedes Element
const verdoppelt = zahlen.map(n => n * 2);
// [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

// filter: Selektiert Elemente basierend auf einer Bedingung
const geradeZahlen = zahlen.filter(n => n % 2 === 0);
// [2, 4, 6, 8, 10]

// reduce: Reduziert das Array auf einen einzelnen Wert
const summe = zahlen.reduce((acc, n) => acc + n, 0);
// 55

// find: Findet das erste Element, das eine Bedingung erfüllt
const erstesGerade = zahlen.find(n => n % 2 === 0);
// 2

// every: Prüft, ob alle Elemente eine Bedingung erfüllen
const allePositiv = zahlen.every(n => n > 0);
// true

// some: Prüft, ob mindestens ein Element eine Bedingung erfüllt
const mindestensEinGerade = zahlen.some(n => n % 2 === 0);
// true

// Verkettung von Methoden
const summeGeradeVerdoppelt = zahlen
  .filter(n => n % 2 === 0)
  .map(n => n * 2)
  .reduce((acc, n) => acc + n, 0);
// 60

22.3.2 Arbeiten mit Objekten

const benutzer = {
  name: "Max Mustermann",
  alter: 30,
  email: "max@beispiel.de",
  rolle: "Admin"
};

// Object.keys, Object.values, Object.entries
const eigenschaften = Object.keys(benutzer);
// ["name", "alter", "email", "rolle"]

const werte = Object.values(benutzer);
// ["Max Mustermann", 30, "max@beispiel.de", "Admin"]

const eintraege = Object.entries(benutzer);
// [["name", "Max Mustermann"], ["alter", 30], ...]

// Funktionaler Ansatz zur Transformation eines Objekts
const benutzerMitVolljährigkeit = Object.entries(benutzer)
  .reduce((obj, [key, value]) => {
    obj[key] = value;
    if (key === "alter") {
      obj.istVolljaehrig = value >= 18;
    }
    return obj;
  }, {});
// { name: "Max Mustermann", alter: 30, ... istVolljaehrig: true }

22.4 Unveränderliche (Immutable) Datenstrukturen

Um Seiteneffekte zu vermeiden, ist es wichtig, mit unveränderlichen Datenstrukturen zu arbeiten.

22.4.1 Unveränderliche Updates für Arrays

const originalArray = [1, 2, 3, 4, 5];

// Hinzufügen eines Elements
const neuesArray1 = [...originalArray, 6];
// Oder
const neuesArray2 = originalArray.concat(6);

// Entfernen eines Elements
const ohneElement = originalArray.filter(item => item !== 3);

// Ersetzen eines Elements
const ersetzt = originalArray.map(item => item === 3 ? 30 : item);

// Aktualisieren eines Elements an einem bestimmten Index
const aktualisiert = [
  ...originalArray.slice(0, 2),
  30, // Neuer Wert für Index 2
  ...originalArray.slice(3)
];

22.4.2 Unveränderliche Updates für Objekte

const originalObjekt = {
  name: "Max",
  alter: 30,
  adresse: {
    strasse: "Hauptstraße",
    hausnummer: 42,
    stadt: "Berlin"
  }
};

// Flaches Update
const aktualisiertesObjekt = {
  ...originalObjekt,
  alter: 31,
  beruf: "Entwickler"
};

// Verschachteltes Update
const mitNeuerAdresse = {
  ...originalObjekt,
  adresse: {
    ...originalObjekt.adresse,
    hausnummer: 43,
    plz: "10115"
  }
};

22.5 Fortgeschrittene funktionale Konzepte

22.5.1 Lazy Evaluation mit Generatoren

Generatoren ermöglichen die faule Auswertung (Lazy Evaluation), bei der Werte nur bei Bedarf berechnet werden:

function* unendlicheFibonacci() {
  let a = 0, b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const fibGenerator = unendlicheFibonacci();
for (let i = 0; i < 10; i++) {
  console.log(fibGenerator.next().value);
}
// 0, 1, 1, 2, 3, 5, 8, 13, 21, 34

22.5.2 Monaden

Monaden sind ein fortgeschrittenes Konzept aus der funktionalen Programmierung, das Berechnungen in einem Kontext kapselt:

// Eine einfache Maybe-Monade
class Maybe {
  constructor(value) {
    this._value = value;
  }

  static of(value) {
    return new Maybe(value);
  }

  isNothing() {
    return this._value === null || this._value === undefined;
  }

  map(fn) {
    if (this.isNothing()) {
      return this;
    }
    return Maybe.of(fn(this._value));
  }

  getOrElse(defaultValue) {
    return this.isNothing() ? defaultValue : this._value;
  }
}

// Verwendung
const result = Maybe.of(5)
  .map(x => x * 2)
  .map(x => x + 10)
  .getOrElse(0);
console.log(result); // 20

const safeResult = Maybe.of(null)
  .map(x => x * 2) // Wird übersprungen
  .map(x => x + 10) // Wird übersprungen
  .getOrElse(0);
console.log(safeResult); // 0

22.5.3 Funktionales Error-Handling mit Either

Das Either-Pattern ist ein funktionales Muster zur Fehlerbehandlung:

class Either {
  static left(value) {
    return {
      isLeft: true,
      isRight: false,
      value,
      map: _ => Either.left(value),
      flatMap: _ => Either.left(value),
      getOrElse: defaultValue => defaultValue,
      fold: (leftFn, _) => leftFn(value)
    };
  }

  static right(value) {
    return {
      isLeft: false,
      isRight: true,
      value,
      map: fn => Either.right(fn(value)),
      flatMap: fn => fn(value),
      getOrElse: _ => value,
      fold: (_, rightFn) => rightFn(value)
    };
  }
}

// Verwendung
function divide(a, b) {
  if (b === 0) {
    return Either.left("Division durch Null");
  }
  return Either.right(a / b);
}

const erfolg = divide(10, 2)
  .map(result => result * 2)
  .fold(
    error => `Fehler: ${error}`,
    value => `Ergebnis: ${value}`
  );
console.log(erfolg); // "Ergebnis: 10"

const fehler = divide(10, 0)
  .map(result => result * 2)
  .fold(
    error => `Fehler: ${error}`,
    value => `Ergebnis: ${value}`
  );
console.log(fehler); // "Fehler: Division durch Null"

22.6 Praktische Beispiele für funktionale Programmierung

22.6.1 Beispiel 1: Datenverarbeitung

// Angenommen, wir haben eine Liste von Produkten
const produkte = [
  { id: 1, name: "Laptop", preis: 1200, kategorie: "Elektronik" },
  { id: 2, name: "Schreibtisch", preis: 350, kategorie: "Möbel" },
  { id: 3, name: "Maus", preis: 25, kategorie: "Elektronik" },
  { id: 4, name: "Tastatur", preis: 80, kategorie: "Elektronik" },
  { id: 5, name: "Stuhl", preis: 150, kategorie: "Möbel" },
  { id: 6, name: "Monitor", preis: 300, kategorie: "Elektronik" }
];

// Funktionale Verarbeitung:
// 1. Filtere nach Elektronikprodukten
// 2. Berechne den Preis mit Mehrwertsteuer
// 3. Sortiere nach Preis (aufsteigend)
// 4. Projiziere auf ein einfacheres Format

const ergebnis = produkte
  .filter(produkt => produkt.kategorie === "Elektronik")
  .map(produkt => ({
    ...produkt,
    preisInklMwSt: Math.round(produkt.preis * 1.19 * 100) / 100
  }))
  .sort((a, b) => a.preisInklMwSt - b.preisInklMwSt)
  .map(produkt => ({
    name: produkt.name,
    preis: produkt.preisInklMwSt
  }));

console.log(ergebnis);
/*
[
  { name: "Maus", preis: 29.75 },
  { name: "Tastatur", preis: 95.2 },
  { name: "Monitor", preis: 357 },
  { name: "Laptop", preis: 1428 }
]
*/

22.6.2 Beispiel 2: Asynchrone Operationen mit funktionalem Ansatz

// Fictive API-Funktionen
const fetchUser = id => fetch(`/api/users/${id}`).then(r => r.json());
const fetchPosts = userId => fetch(`/api/users/${userId}/posts`).then(r => r.json());
const fetchComments = postId => fetch(`/api/posts/${postId}/comments`).then(r => r.json());

// Funktionale Hilfsfunktionen
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const prop = key => obj => obj[key];
const map = fn => array => array.map(fn);
const flatMap = fn => array => array.flatMap(fn);
const first = array => array[0];

// Asynchrone Pipeline mit Promises
async function getUserFirstPostComments(userId) {
  return pipe(
    fetchUser,
    user => fetchPosts(user.id),
    posts => first(posts),
    post => fetchComments(post.id)
  )(userId);
}

// Verwendung
getUserFirstPostComments(123)
  .then(comments => console.log("Kommentare:", comments))
  .catch(error => console.error("Fehler:", error));

22.6.3 Beispiel 3: Event Handling mit funktionalem Ansatz

// Funktionen zur Ereignisverarbeitung
const preventDefault = event => {
  event.preventDefault();
  return event;
};

const extractFormData = event => {
  const form = event.target;
  const formData = new FormData(form);
  return {
    event,
    formData: Object.fromEntries(formData.entries())
  };
};

const validateForm = ({ event, formData }) => {
  const errors = {};
  
  if (!formData.name || formData.name.length < 3) {
    errors.name = "Name muss mindestens 3 Zeichen lang sein";
  }
  
  if (!formData.email || !formData.email.includes('@')) {
    errors.email = "Ungültige E-Mail-Adresse";
  }
  
  return {
    event,
    formData,
    errors,
    isValid: Object.keys(errors).length === 0
  };
};

const submitForm = ({ event, formData, errors, isValid }) => {
  if (!isValid) {
    return { success: false, errors };
  }
  
  // API-Aufruf würde hier stattfinden
  console.log("Formular wird gesendet:", formData);
  
  return { success: true, data: formData };
};

// Zusammengesetzte Funktion
const handleSubmit = pipe(
  preventDefault,
  extractFormData,
  validateForm,
  submitForm
);

// Verbindung mit dem DOM
document.getElementById('userForm').addEventListener('submit', event => {
  const result = handleSubmit(event);
  
  if (result.success) {
    showSuccessMessage("Formular erfolgreich gesendet!");
  } else {
    showErrors(result.errors);
  }
});

22.7 Funktionale Programmierung mit modernen JavaScript-Features

22.7.1 Optional Chaining und Nullish Coalescing

Diese modernen JavaScript-Features ergänzen funktionale Programmierung durch sicherere Zugriffe auf Objekte:

// Optional Chaining (?.)
const benutzer = {
  profil: {
    adresse: {
      stadt: "Berlin"
    }
  }
};

// Traditioneller defensiver Ansatz
const stadt1 = benutzer && 
               benutzer.profil && 
               benutzer.profil.adresse && 
               benutzer.profil.adresse.stadt || 
               "Unbekannt";

// Mit Optional Chaining
const stadt2 = benutzer?.profil?.adresse?.stadt ?? "Unbekannt";

22.7.2 Destructuring und Spread-Operator

Diese Features erleichtern das Arbeiten mit unveränderlichen Datenstrukturen:

// Destructuring für leichteren Zugriff auf Objekteigenschaften
function verarbeiteBenutzer({ name, alter, email }) {
  return `${name} (${alter}) - ${email}`;
}

// Spread für unveränderliche Updates
function aktualisiereBenutzer(benutzer, updates) {
  return { ...benutzer, ...updates };
}

const benutzer = { name: "Max", alter: 30, email: "max@example.com" };
const aktualisiert = aktualisiereBenutzer(benutzer, { alter: 31 });

22.8 Vor- und Nachteile der funktionalen Programmierung

22.8.1 Vorteile

  1. Vorhersehbarkeit: Pure Funktionen erzeugen bei gleichen Eingaben immer gleiche Ausgaben
  2. Testbarkeit: Funktionen ohne Seiteneffekte sind einfacher zu testen
  3. Parallelisierbarkeit: Keine gemeinsamen Zustände bedeuten weniger Probleme bei paralleler Ausführung
  4. Modularität: Funktionen können leicht kombiniert und wiederverwendet werden
  5. Fehlerbehandlung: Funktionale Muster wie Maybe und Either verbessern das Fehlerhandling
  6. Wartbarkeit: Deklarativer Code ist oft leichter zu verstehen und zu warten

22.8.2 Nachteile

  1. Lernkurve: Funktionale Konzepte können für Anfänger schwer zu verstehen sein
  2. Ressourcenverbrauch: Unveränderliche Datenstrukturen können mehr Speicher benötigen
  3. Leistung: Manche funktionale Operationen können weniger performant sein als imperative Ansätze
  4. Kompatibilität: Nicht alle JavaScript-Umgebungen unterstützen alle modernen Features

22.9 Funktionale Bibliotheken für JavaScript

Es gibt verschiedene Bibliotheken, die funktionale Programmierung in JavaScript unterstützen:

  1. Lodash/FP: Eine funktionale Version der beliebten Lodash-Bibliothek
  2. Ramda: Eine Bibliothek, die speziell für funktionale Programmierung entwickelt wurde
  3. Immutable.js: Bietet unveränderliche Datenstrukturen für JavaScript
  4. Mori: Clojure’s persistente Datenstrukturen für JavaScript

Beispiel mit Ramda:

const R = require('ramda');

const benutzer = [
  { name: "Max", alter: 30, aktiv: true },
  { name: "Anna", alter: 25, aktiv: false },
  { name: "Tom", alter: 40, aktiv: true }
];

// Finde aktive Benutzer und extrahiere ihre Namen
const aktiveBenutzernamen = R.pipe(
  R.filter(R.prop('aktiv')),
  R.map(R.prop('name')),
  R.join(', ')
)(benutzer);

console.log(aktiveBenutzernamen); // "Max, Tom"