Currying und partielle Anwendung in PHP
AI generated
<?php
8.4
PHP · Funktionale Programmierung · Closures
Currying und partielle Anwendung in PHP
Funktionen schrittweise spezialisieren

Currying zerlegt eine mehrstellige Funktion in eine Kette einstelliger Funktionen, partielle Anwendung friert einzelne Argumente vorab ein. Beide Techniken lassen sich in PHP 8.4 ohne externe Bibliothek mit Closures nachbauen und reduzieren wiederkehrende Parameter auf ein Minimum.

17 Min. Lesezeit curry() · partial application · Closures PHP 8.2 · 8.3 · 8.4

1. Currying und partielle Anwendung: Begriffe sauber trennen

Currying bezeichnet die Umwandlung einer Funktion mit mehreren Parametern in eine Kette von Funktionen, von denen jede genau ein Argument entgegennimmt und die nächste Funktion in der Kette zurückgibt, bis alle Argumente verbraucht sind. Eine Funktion add(int $a, int $b): int wird durch Currying zu fn($a) => fn($b) => $a + $b. Erst der letzte Aufruf liefert das eigentliche Ergebnis, jeder Zwischenschritt liefert eine neue, spezialisierte Funktion.

Partielle Anwendung ist verwandt, aber nicht identisch: hier wird eine Funktion mit einer beliebigen Teilmenge ihrer Argumente aufgerufen, und das Ergebnis ist eine neue Funktion, die nur noch die restlichen Argumente erwartet, egal in welcher Anzahl. Während Currying immer strikt ein Argument pro Schritt verarbeitet, kann partielle Anwendung mehrere Argumente auf einmal binden. In PHP werden beide Techniken nicht nativ unterstützt, lassen sich aber mit wenigen Zeilen Closure-Code vollständig nachbauen.

Der praktische Nutzen liegt darin, aus einer generischen Funktion spezialisierte Varianten zu erzeugen, ohne Code zu duplizieren. Eine generische Steuerformel wird durch Currying und partielle Anwendung zu einer auf einen bestimmten Steuersatz spezialisierten Funktion, die überall dort eingesetzt werden kann, wo dieser Satz gilt. Die folgenden Abschnitte zeigen den Aufbau von Currying in PHP Schritt für Schritt, von der manuellen Variante bis zum generischen Helper.

2. Manuelles Currying mit verschachtelten Closures

Der einfachste Einstieg in Currying ist eine Funktion, die von Hand eine Closure zurückgibt, welche wiederum eine weitere Closure zurückgibt. Für eine feste Anzahl von Parametern ist das unkompliziert und vollständig typisierbar, ohne generische Hilfsfunktionen zu benötigen. Diese manuelle Variante eignet sich besonders für Funktionen mit zwei oder drei Parametern, bei denen ein generischer Currying-Mechanismus mehr Komplexität einführen würde, als er einspart.

Der Nachteil der manuellen Variante: für jede Parameteranzahl muss eine eigene Verschachtelungstiefe geschrieben werden. Eine Funktion mit vier Parametern benötigt vier ineinander verschachtelte Closures, was schnell unübersichtlich wird. Für diesen Fall lohnt sich der generische Ansatz aus dem nächsten Abschnitt, aber das manuelle Muster bleibt die klarste Grundlage, um Currying konzeptionell zu verstehen.


<?php

declare(strict_types=1);

/**
 * Manually curried three-argument function for tax calculation.
 *
 * @return Closure(float): Closure(float): float
 */
function curriedTax(float $rate): Closure
{
    return function (float $net) use ($rate): Closure {
        return function (float $shippingCost) use ($rate, $net): float {
            $gross = $net * (1 + $rate);
            return $gross + $shippingCost;
        };
    };
}

// Step by step application — each call returns a new, more specific closure
$withGermanVat = curriedTax(0.19);
$withNetPrice = $withGermanVat(49.90);
$total = $withNetPrice(4.90);

echo number_format($total, 2); // 63.29

// Or fully applied in a single chained expression
$totalDirect = curriedTax(0.19)(49.90)(4.90);

3. Ein generischer curry()-Helper für beliebige Funktionen

Statt für jede Funktion manuell verschachtelte Closures zu schreiben, lässt sich Currying mit einem einzigen generischen Helper für beliebige Funktionen automatisieren. Die Kernidee: eine curry()-Funktion analysiert über Reflection, wie viele Parameter die Zielfunktion erwartet, und gibt eine Closure zurück, die so lange weitere Closures produziert, bis genug Argumente gesammelt wurden, um die ursprüngliche Funktion tatsächlich aufzurufen.

Dieser generische Helper macht Currying für jede beliebige Funktion nutzbar, ohne dass für jede Arity eine eigene Implementierung geschrieben werden muss. Der Preis dafür ist etwas Reflection-Overhead beim ersten Aufruf und ein Verlust an statischer Typisierbarkeit gegenüber der manuellen Variante, weil PHPStan die generische Signatur nicht ohne Weiteres in eine spezifische Kette auflösen kann.


<?php

declare(strict_types=1);

/**
 * Generic curry helper — works with any closure regardless of arity.
 *
 * @param Closure $fn
 * @param int|null $arity Number of parameters, auto-detected if omitted.
 * @return Closure
 */
function curry(Closure $fn, ?int $arity = null): Closure
{
    $arity ??= (new ReflectionFunction($fn))->getNumberOfParameters();

    $collector = function (array $collected) use ($fn, $arity, &$collector): mixed {
        if (count($collected) >= $arity) {
            return $fn(...$collected);
        }

        return function (mixed ...$args) use ($collected, $collector): mixed {
            return $collector([...$collected, ...$args]);
        };
    };

    return $collector([]);
}

$add3 = fn (int $a, int $b, int $c): int => $a + $b + $c;
$curriedAdd3 = curry($add3);

// Any combination of step sizes works
echo $curriedAdd3(1)(2)(3);       // 6
echo $curriedAdd3(1, 2)(3);       // 6
echo $curriedAdd3(1)(2, 3);       // 6
echo $curriedAdd3(1, 2, 3);       // 6

4. Partielle Anwendung: Argumente vorab einfrieren

Partielle Anwendung unterscheidet sich von Currying dadurch, dass eine beliebige Teilmenge der Argumente auf einmal gebunden wird, ohne dass die Funktion zwingend genau ein Argument pro Aufruf erwartet. Eine partial()-Funktion nimmt eine Funktion sowie eine feste Menge von Argumenten entgegen und liefert eine neue Funktion, die nur noch die verbleibenden Parameter benötigt. Das ist besonders nützlich, um Konfigurationswerte wie eine Basis-URL, einen Mandanten oder eine Sprache einmalig festzulegen.

Der Unterschied wird deutlich, wenn eine Funktion mit fünf Parametern nur mit den ersten beiden vorbelegt werden soll: partielle Anwendung erledigt das in einem einzigen Aufruf, während striktes Currying zwei einzelne Aufrufe erfordern würde. In der Praxis kombiniert man häufig beide Techniken, etwa partielle Anwendung für die grobe Vorkonfiguration und anschließendes Currying für die feingranulare, schrittweise Übergabe der restlichen Werte.


<?php

declare(strict_types=1);

/**
 * Partial application: pre-binds a fixed set of leading arguments.
 *
 * @param Closure $fn
 * @param mixed ...$boundArgs
 * @return Closure
 */
function partial(Closure $fn, mixed ...$boundArgs): Closure
{
    return function (mixed ...$remainingArgs) use ($fn, $boundArgs): mixed {
        return $fn(...$boundArgs, ...$remainingArgs);
    };
}

function buildApiUrl(string $baseUrl, string $tenant, string $resource, int $id): string
{
    return sprintf('%s/%s/%s/%d', $baseUrl, $tenant, $resource, $id);
}

// Bind base URL and tenant once, reuse for many resources
$tenantApi = partial(buildApiUrl(...), 'https://api.mironsoft.de', 'acme-shop');

echo $tenantApi('orders', 42);    // https://api.mironsoft.de/acme-shop/orders/42
echo $tenantApi('customers', 7);  // https://api.mironsoft.de/acme-shop/customers/7

5. Praxisbeispiel: Preisregeln und Konfigurationsvarianten

Ein realistisches Anwendungsfeld für Currying und partielle Anwendung ist die Preisberechnung mit variablen Rabattregeln pro Kundengruppe. Statt für jede Kundengruppe eine eigene Preisfunktion zu schreiben, definiert man eine generische Rabattfunktion und erzeugt durch partielle Anwendung spezialisierte Varianten für Stammkunden, Neukunden oder Großhandelspartner. Jede Variante bleibt eine ganz normale, aufrufbare Funktion, ohne dass eine zusätzliche Klasse benötigt wird.

Dieses Muster reduziert die Anzahl der Codepfade drastisch: Anstatt zehn ähnlicher Funktionen mit fast identischer Logik existiert eine einzige generische Funktion und zehn kleine, durch Currying oder partielle Anwendung erzeugte Spezialisierungen. Änderungen an der Kernlogik müssen dadurch nur an einer einzigen Stelle vorgenommen werden.


<?php

declare(strict_types=1);

/**
 * @return Closure(float): float
 */
function discountCalculator(float $percentage, float $maxDiscountAmount): Closure
{
    return function (float $price) use ($percentage, $maxDiscountAmount): float {
        $discount = min($price * $percentage, $maxDiscountAmount);
        return round($price - $discount, 2);
    };
}

// Curried factory produces specialized pricing functions per customer group
$curriedDiscount = curry(discountCalculator(...));

$loyaltyDiscount = $curriedDiscount(0.10)(50.0);   // 10%, capped at 50 EUR
$wholesaleDiscount = $curriedDiscount(0.25)(500.0); // 25%, capped at 500 EUR

echo $loyaltyDiscount(199.0);   // 179.10
echo $wholesaleDiscount(2000.0); // 1500.00 (capped)

6. Typisierung mit PHPStan: Closure-Signaturen über Stufen

Der generische curry()-Helper hat einen strukturellen Nachteil: sein Rückgabetyp ist entweder das endgültige Ergebnis oder eine weitere Closure, was sich mit einem einfachen PHPDoc-Typ nicht präzise ausdrücken lässt. Für Funktionen mit fester, kleiner Arity ist es deshalb sinnvoll, eigene typisierte Wrapper zu schreiben, die die konkrete Signatur jeder Stufe des Currying-Prozesses in PHPDoc festhalten, etwa Closure(float): Closure(float): float für eine zweistufige Kette.

Für den generischen Fall bleibt callable oder Closure als Rückgabetyp mit einem erklärenden Kommentar die pragmatische Lösung. PHPStan kann so zwar nicht jede Zwischenstufe prüfen, verhindert aber zumindest, dass ein Nicht-Callable versehentlich als Ergebnis eines Currying-Aufrufs weitergereicht wird. Wo maximale Typsicherheit gefragt ist, sind manuell geschriebene, kleine curry-Funktionen der generischen Lösung vorzuziehen.


<?php

declare(strict_types=1);

/**
 * Explicitly typed two-step curry for maximum PHPStan precision.
 *
 * @return Closure(float): Closure(float): float
 */
function curry2(Closure $fn): Closure
{
    return function (float $a) use ($fn): Closure {
        return function (float $b) use ($fn, $a): float {
            return $fn($a, $b);
        };
    };
}

$divide = fn (float $numerator, float $denominator): float => $numerator / $denominator;
$curriedDivide = curry2($divide);

$divideBy100 = fn (float $numerator): float => $curriedDivide($numerator)(100.0);
echo $divideBy100(2500.0); // 25.0

7. Named Arguments als Alternative und Ergänzung

Named Arguments, seit PHP 8.0 verfügbar, lösen ein ähnliches Problem wie partielle Anwendung, allerdings mit anderen Mitteln: statt eine neue Funktion zu erzeugen, erlauben sie, beim Aufruf nur die tatsächlich abweichenden Parameter anzugeben, während der Rest auf Standardwerten verbleibt. Für Funktionen mit vielen optionalen Parametern ist das oft die einfachere und lesbarere Lösung als ein Currying-Konstrukt.

Der entscheidende Unterschied: Named Arguments erzeugen keine wiederverwendbare, benannte Zwischenfunktion. Wer eine Spezialisierung mehrfach im Code verwenden möchte, etwa $wholesalePrice = ... als eigenständige Variable, profitiert weiterhin von partieller Anwendung oder Currying, weil das Ergebnis eine benannte, wiederverwendbare Closure ist, während Named Arguments nur die Lesbarkeit eines einzelnen Aufrufs verbessern.


<?php

declare(strict_types=1);

function calculatePrice(
    float $netPrice,
    float $vatRate = 0.19,
    float $shippingCost = 0.0,
    float $discountPercentage = 0.0,
): float {
    $discounted = $netPrice * (1 - $discountPercentage);
    return round($discounted * (1 + $vatRate) + $shippingCost, 2);
}

// Named arguments: readable one-off call, no reusable function produced
$price = calculatePrice(netPrice: 49.90, discountPercentage: 0.10);

// Partial application: produces a reusable, named closure instead
$wholesalePrice = partial(calculatePrice(...), 49.90, 0.19, 0.0, 0.25);
echo $wholesalePrice(); // reusable across the codebase

8. Fallstricke: Reihenfolge, Arity und Debugging

Der häufigste Fehler beim generischen curry()-Helper ist eine falsch erkannte Arity bei Funktionen mit optionalen Parametern. ReflectionFunction::getNumberOfParameters() zählt auch Parameter mit Standardwert mit, wodurch der Helper unter Umständen mehr Argumente erwartet, als für einen sinnvollen Aufruf nötig wären. Für solche Funktionen ist es sicherer, die Arity explizit als zweiten Parameter an curry() zu übergeben, statt sich auf die automatische Erkennung zu verlassen.

Ein zweiter Fallstrick ist die Argumentreihenfolge bei partieller Anwendung: da Argumente von links nach rechts gebunden werden, muss die Zielfunktion so entworfen sein, dass die am häufigsten wiederverwendeten, stabilen Werte an den ersten Positionen stehen und die variablen Werte am Ende. Debugging von tief verschachtelten Currying-Ketten gestaltet sich schwieriger als bei normalen Funktionsaufrufen, weil ein Stacktrace mehrere anonyme Closure-Frames statt eines einzigen benannten Funktionsaufrufs zeigt. Aussagekräftige Variablennamen für jede Zwischenstufe mindern dieses Problem spürbar.

9. Currying, partielle Anwendung und Alternativen im Vergleich

Die Wahl zwischen Currying, partieller Anwendung und Named Arguments hängt vom konkreten Anwendungsfall ab. Die folgende Tabelle stellt die Techniken gegenüber und zeigt, wann welche Variante die geeignetste ist.

Situation Technik Ergebnis Wiederverwendbar
Einzelaufruf mit vielen Standardwerten Named Arguments Direktes Ergebnis Nein, kein Objekt entsteht
Feste Vorbelegung, mehrfach genutzt Partielle Anwendung Neue Closure Ja, als Variable speicherbar
Schrittweise Spezialisierung, ein Argument nach dem anderen Currying Kette von Closures Ja, jede Zwischenstufe
Zwei bis drei Parameter, feste Arity Manuelles Currying Voll typisiert Ja, präzise für PHPStan
Beliebige, unbekannte Arity Generischer curry()-Helper Flexibel, weniger typsicher Ja, mit Reflection-Overhead

Für die meisten realen Projekte ist eine Kombination die beste Wahl: partielle Anwendung für feste Konfigurationswerte wie Mandant oder Basis-URL, manuelles Currying für kleine, häufig verwendete Funktionen mit stabiler Arity, und Named Arguments für alle übrigen Einzelaufrufe, bei denen keine wiederverwendbare Funktion benötigt wird.

Mironsoft

PHP-Architektur, Code-Reviews und moderne Sprachfeatures im Team-Alltag

Konfigurationslogik ohne Duplikation spezialisieren?

Wir zeigen, wo Currying und partielle Anwendung wiederkehrende Preisregeln, Mandanten-Konfigurationen und API-Wrapper in eurem PHP-Code auf wenige generische Funktionen reduzieren.

Code-Review

Duplizierte Parametrisierungen identifizieren und auf Currying-Muster prüfen

Refactoring

Generische Funktionen mit partieller Anwendung in spezialisierte Varianten überführen

Schulung

Funktionale Muster praxisnah einführen, mit typisierten PHPStan-Beispielen

10. Zusammenfassung

Currying zerlegt eine mehrstellige Funktion in eine Kette einstelliger Funktionsaufrufe, während partielle Anwendung eine beliebige Teilmenge von Argumenten auf einmal bindet. Beide Techniken lassen sich in PHP 8.4 vollständig mit Closures nachbauen, entweder manuell für feste, kleine Arity oder generisch über Reflection für beliebige Funktionen. Der generische curry()-Helper bietet maximale Flexibilität, während manuell geschriebene, kleine Varianten die präzisere PHPStan-Typisierung erlauben.

In der Praxis bewähren sich Currying und partielle Anwendung überall dort, wo aus einer generischen Funktion mehrere spezialisierte, wiederverwendbare Varianten entstehen sollen, etwa bei Preisregeln, API-Wrappern oder Mandanten-Konfigurationen. Named Arguments bleiben die bessere Wahl für einzelne, nicht wiederverwendete Aufrufe. Wer beide Werkzeuge kombiniert einsetzt, reduziert Duplikation, ohne die Typsicherheit des Codes zu opfern.

Currying und partielle Anwendung in PHP — Das Wichtigste auf einen Blick

Currying

Zerlegt eine Funktion in eine Kette einstelliger Aufrufe, jeder Zwischenschritt liefert eine neue Closure.

Partielle Anwendung

Bindet eine beliebige Teilmenge von Argumenten auf einmal, unabhängig von der Anzahl pro Schritt.

Generischer Helper

curry() mit Reflection funktioniert für jede Arity, kostet aber statische Typsicherheit.

Named Arguments

Bessere Wahl für einzelne, nicht wiederverwendete Aufrufe mit vielen optionalen Parametern.

11. FAQ: Currying und partielle Anwendung in PHP

1Currying vs. partielle Anwendung?
Currying verarbeitet strikt ein Argument pro Schritt. Partielle Anwendung bindet beliebig viele Argumente auf einmal.
2Unterstützt PHP Currying nativ?
Nein, es gibt kein natives Sprachfeature. Nachbau mit wenigen Zeilen Closure-Code, manuell oder generisch.
3Generischer Helper oder manuelle Closures?
Generisch bei wechselnder Arity, manuell und typisiert bei zwei bis drei festen Parametern für präzisere PHPStan-Prüfung.
4Wie ermittelt curry() die Parameteranzahl?
Über ReflectionFunction::getNumberOfParameters(), das auch optionale Parameter mitzählt und dadurch die Arity verfälschen kann.
5Vorteil ggü. Named Arguments?
Partielle Anwendung erzeugt eine wiederverwendbare Closure. Named Arguments verbessern nur einen einzelnen Aufruf.
6Können beide kombiniert werden?
Ja, oft erst partielle Anwendung für Grobkonfiguration, dann Currying für feingranulare, schrittweise Übergabe.
7Spürbare Performance-Kosten?
Nein, minimaler Reflection-Overhead beim ersten Aufruf, in der Praxis für die meisten Anwendungen irrelevant.
8Debugging tiefer Ketten?
Aussagekräftige Variablennamen je Zwischenstufe vergeben, da der Stacktrace sonst nur anonyme Closure-Frames zeigt.
9Typsicher mit PHPStan?
Manuelle Zwischenstufen ja, mit präzisem PHPDoc. Der generische Helper lässt sich nur grob als Closure typisieren.
10Wo sinnvoll in echten Projekten?
Preisregeln mit variablen Rabatten, API-Wrapper mit fester Basis-URL pro Mandant, spezialisierte Validierungsfunktionen.