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.
Inhaltsverzeichnis
- 1. Currying und partielle Anwendung: Begriffe sauber trennen
- 2. Manuelles Currying mit verschachtelten Closures
- 3. Ein generischer curry()-Helper für beliebige Funktionen
- 4. Partielle Anwendung: Argumente vorab einfrieren
- 5. Praxisbeispiel: Preisregeln und Konfigurationsvarianten
- 6. Typisierung mit PHPStan: Closure-Signaturen über Stufen
- 7. Named Arguments als Alternative und Ergänzung
- 8. Fallstricke: Reihenfolge, Arity und Debugging
- 9. Currying, partielle Anwendung und Alternativen im Vergleich
- 10. Zusammenfassung
- 11. FAQ
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.