Kodokon kodokon.com

Funktionale Muster: Currying, Komposition, Unveränderlichkeit

Strukturiere deinen Code mit Currying, Funktionskomposition und unveränderlichen Daten, um Vorhersehbarkeit und Testbarkeit zu gewinnen.

9 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

JavaScript behandelt Funktionen als First-Class-Werte, was die Tür zu funktionalen Mustern öffnet. Currying verwandelt eine Funktion mit n Argumenten in eine Kette von Funktionen mit einem einzigen Argument: f(a, b, c) wird zu f(a)(b)(c). Nicht zu verwechseln mit der partiellen Anwendung, die einige Argumente in einem einzigen Schritt festlegt (fn.bind(null, a)). Currying glänzt darin, aus generischen Funktionen spezialisierte zu erzeugen.

JAVASCRIPT
const curry = (fn) => {
  const arity = fn.length;
  return function curried(...args) {
    if (args.length >= arity) return fn(...args);
    return (...rest) => curried(...args, ...rest);
  };
};

const add3 = (a, b, c) => a + b + c;
const addCurried = curry(add3);

console.log(addCurried(1)(2)(3)); // 6
console.log(addCurried(1, 2)(3)); // 6
Ein generisches curry, basierend auf der Arität der Funktion.

Eine Feinheit der Spezifikation: fn.length zählt nur die Parameter, die vor dem ersten Default- oder Rest-Parameter stehen. (a, b = 1) => {} hat eine length von 1, (...args) => {} von 0. Einem aritätsbasierten curry muss man daher Funktionen mit einfachen Parametern geben. Komposition ist der nächste Schritt: kleine reine Funktionen zu lesbaren Pipelines zusammenzufügen.

JAVASCRIPT
const pipe = (...fns) => (input) =>
  fns.reduce((acc, fn) => fn(acc), input);

const trim = (s) => s.trim();
const lower = (s) => s.toLowerCase();
const dashes = (s) => s.replace(/\s+/g, "-");

const toSlug = pipe(trim, lower, dashes);
console.log(toSlug("  Hello World  "));
// "hello-world"
pipe wendet die Funktionen von links nach rechts an.

pipe(f, g, h)(x) berechnet h(g(f(x))): Du liest es in Ausführungsreihenfolge. Die Variante compose wendet von rechts nach links an, wie in der mathematischen Notation. Diese Pipelines sind nur wertvoll, wenn die Funktionen rein sind - keine Seiteneffekte, keine versteckten Abhängigkeiten -, was unveränderliche Daten erfordert: Statt ein Objekt zu mutieren, erzeugst du eine neue Version davon, und Referenzgleichheit genügt dann, um eine Änderung zu erkennen (das Herzstück von Reacts Reconciliation).

JAVASCRIPT
const state = Object.freeze({
  user: { name: "Ada" },
  tags: ["js"]
});

const next = {
  ...state,
  tags: [...state.tags, "fp"]
};

const deepCopy = structuredClone(state);
deepCopy.user.name = "Grace"; // independent copy
Eine neue Version durch Kopieren statt durch Mutation.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Was ist der Unterschied zwischen Currying und partieller Anwendung?
    • Keiner, sie sind Synonyme
    • Currying verwandelt f(a, b, c) in f(a)(b)(c); partielle Anwendung legt einige Argumente in einem einzigen Aufruf fest
    • Partielle Anwendung funktioniert nur mit bind, Currying nur mit Arrow-Funktionen
    • Currying gilt nur für Funktionen mit zwei Argumenten
  2. Wozu ist pipe(f, g, h)(x) gleichwertig?
    • f(g(h(x)))
    • h(g(f(x)))
    • g(f(h(x)))
    • f(x) + g(x) + h(x)
  3. Funktioniert nach const s = Object.freeze({ a: { b: 1 } }) die Zuweisung s.a.b = 2?
    • Nein, freeze friert den gesamten Objektbaum ein
    • Nein, es wird sogar außerhalb des Strict Mode ein TypeError geworfen
    • Ja, weil freeze flach ist: Das verschachtelte Objekt s.a ist nicht eingefroren
    • Ja, aber nur im Nicht-Strict-Mode