Specializing Functions Step by Step
Currying breaks a multi-argument function into a chain of single-argument functions, partial application freezes some arguments upfront. Both techniques can be rebuilt in PHP 8.4 without any external library, using closures, and they cut repeated parameters down to a minimum.
Table of Contents
- 1. Currying and Partial Application: Separating the Terms Cleanly
- 2. Manual Currying with Nested Closures
- 3. A Generic curry() Helper for Any Function
- 4. Partial Application: Freezing Arguments Upfront
- 5. Practical Example: Pricing Rules and Configuration Variants
- 6. Typing with PHPStan: Closure Signatures Across Stages
- 7. Named Arguments as an Alternative and Complement
- 8. Pitfalls: Order, Arity and Debugging
- 9. Currying, Partial Application and Alternatives Compared
- 10. Summary
- 11. FAQ
1. Currying and Partial Application: Separating the Terms Cleanly
Currying refers to transforming a function with several parameters into a chain of functions, each taking exactly one argument and returning the next function in the chain, until all arguments are used up. A function add(int $a, int $b): int becomes, through currying, fn($a) => fn($b) => $a + $b. Only the final call delivers the actual result, every intermediate step returns a new, specialized function.
Partial application is related but not identical: here a function is called with an arbitrary subset of its arguments, and the result is a new function that only expects the remaining arguments, however many there are. While currying always strictly handles one argument per step, partial application can bind several arguments at once. PHP does not support either technique natively, but both can be fully rebuilt with a few lines of closure code.
The practical benefit lies in producing specialized variants from a generic function without duplicating code. A generic tax formula becomes, through currying and partial application, a function specialized for one specific tax rate that can be used everywhere that rate applies. The following sections show how to build currying in PHP step by step, from the manual variant to a generic helper.
2. Manual Currying with Nested Closures
The simplest entry point into currying is a function that manually returns a closure, which in turn returns another closure. For a fixed number of parameters this is straightforward and fully typeable, without needing any generic helper functions. This manual variant works particularly well for functions with two or three parameters, where a generic currying mechanism would introduce more complexity than it saves.
The downside of the manual variant: every parameter count needs its own nesting depth written by hand. A function with four parameters requires four nested closures, which quickly becomes hard to follow. In that case the generic approach from the next section is worth it, but the manual pattern remains the clearest foundation for understanding currying conceptually.
<?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. A Generic curry() Helper for Any Function
Instead of manually writing nested closures for every function, currying can be automated with a single generic helper that works for arbitrary functions. The core idea: a curry() function inspects, via reflection, how many parameters the target function expects, and returns a closure that keeps producing further closures until enough arguments have been collected to actually call the original function.
This generic helper makes currying usable for any function without writing a separate implementation per arity. The cost is some reflection overhead on the first call and a loss of static typeability compared to the manual variant, since PHPStan cannot easily resolve the generic signature into a specific chain.
<?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. Partial Application: Freezing Arguments Upfront
Partial application differs from currying in that an arbitrary subset of arguments gets bound at once, without the function necessarily expecting exactly one argument per call. A partial() function accepts a function plus a fixed set of arguments and returns a new function that only needs the remaining parameters. This is especially useful for locking in configuration values like a base URL, a tenant, or a language once.
The difference becomes clear when a function with five parameters only needs the first two pre-filled: partial application handles that in a single call, while strict currying would require two separate calls. In practice both techniques are often combined, for example partial application for coarse pre-configuration followed by currying for the fine-grained, step-by-step passing of the remaining values.
<?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. Practical Example: Pricing Rules and Configuration Variants
A realistic application area for currying and partial application is price calculation with variable discount rules per customer group. Instead of writing a separate pricing function for every customer group, you define one generic discount function and produce specialized variants for loyal customers, new customers, or wholesale partners through partial application. Each variant remains a perfectly ordinary, callable function, without needing an extra class.
This pattern drastically reduces the number of code paths: instead of ten similar functions with nearly identical logic, there is one generic function and ten small specializations produced via currying or partial application. Changes to the core logic then only need to be made in a single place.
<?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. Typing with PHPStan: Closure Signatures Across Stages
The generic curry() helper has a structural downside: its return type is either the final result or another closure, which cannot be expressed precisely with a simple PHPDoc type. For functions with a fixed, small arity it is therefore worth writing dedicated typed wrappers that pin down the concrete signature of every stage of the currying process in PHPDoc, for example Closure(float): Closure(float): float for a two-step chain.
For the generic case, callable or Closure as the return type with an explanatory comment remains the pragmatic solution. PHPStan then cannot check every intermediate stage, but it at least prevents a non-callable value from being accidentally passed on as the result of a currying call. Where maximum type safety is required, manually written, small curry functions are preferable to the generic solution.
<?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 as an Alternative and Complement
Named arguments, available since PHP 8.0, solve a similar problem to partial application, but through different means: instead of producing a new function, they let a call specify only the parameters that actually differ, while the rest stay at their default values. For functions with many optional parameters this is often the simpler and more readable solution compared to a currying construct.
The key difference: named arguments do not produce a reusable, named intermediate function. Anyone who wants to reuse a specialization multiple times in the code, for example as a standalone variable such as $wholesalePrice = ..., still benefits from partial application or currying, because the result is a named, reusable closure, while named arguments only improve the readability of a single call.
<?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. Pitfalls: Order, Arity and Debugging
The most common mistake with the generic curry() helper is a misdetected arity on functions with optional parameters. ReflectionFunction::getNumberOfParameters() also counts parameters with a default value, which means the helper may end up expecting more arguments than would be needed for a sensible call. For such functions it is safer to pass the arity explicitly as the second parameter to curry(), instead of relying on automatic detection.
A second pitfall is argument order in partial application: since arguments are bound left to right, the target function needs to be designed so the most commonly reused, stable values sit in the first positions and the variable values come last. Debugging deeply nested currying chains is harder than debugging normal function calls, because a stack trace shows several anonymous closure frames instead of a single named function call. Meaningful variable names for every intermediate stage noticeably ease this problem.
9. Currying, Partial Application and Alternatives Compared
The choice between currying, partial application and named arguments depends on the concrete use case. The following table contrasts the techniques and shows when each variant is the most suitable.
| Situation | Technique | Result | Reusable |
|---|---|---|---|
| Single call with many defaults | Named Arguments | Direct result | No, no object is produced |
| Fixed pre-binding, used repeatedly | Partial Application | New closure | Yes, storable as a variable |
| Step-by-step specialization, one argument at a time | Currying | Chain of closures | Yes, every intermediate stage |
| Two to three parameters, fixed arity | Manual currying | Fully typed | Yes, precise for PHPStan |
| Arbitrary, unknown arity | Generic curry() helper | Flexible, less type-safe | Yes, with reflection overhead |
For most real-world projects a combination is the best choice: partial application for fixed configuration values like tenant or base URL, manual currying for small, frequently used functions with a stable arity, and named arguments for all remaining one-off calls where no reusable function is needed.
Mironsoft
PHP architecture, code reviews and modern language features in everyday team work
Specializing configuration logic without duplication?
We show where currying and partial application can reduce recurring pricing rules, tenant configurations and API wrappers in your PHP code down to a handful of generic functions.
Code Review
Identifying duplicated parameterizations and checking for currying patterns
Refactoring
Turning generic functions into specialized variants using partial application
Training
Introducing functional patterns hands-on, with typed PHPStan examples
10. Summary
Currying breaks a multi-argument function into a chain of single-argument calls, while partial application binds an arbitrary subset of arguments at once. Both techniques can be fully rebuilt in PHP 8.4 using closures, either manually for fixed, small arity or generically via reflection for arbitrary functions. The generic curry() helper offers maximum flexibility, while manually written, small variants allow more precise PHPStan typing.
In practice, currying and partial application prove useful anywhere a generic function should produce several specialized, reusable variants, such as pricing rules, API wrappers, or tenant configurations. Named arguments remain the better choice for single, non-reused calls. Combining both tools reduces duplication without sacrificing the type safety of the code.
Currying and Partial Application in PHP — The Key Points at a Glance
Currying
Breaks a function into a chain of single-argument calls, every intermediate step returns a new closure.
Partial Application
Binds an arbitrary subset of arguments at once, regardless of how many per step.
Generic Helper
curry() with reflection works for any arity, but costs static type safety.
Named Arguments
Better choice for single, non-reused calls with many optional parameters.