====== PHP RFC: Partial Function Application (v2) ====== * Version: 2.0 * Date: 2025-06-03 * Author: Larry Garfield (larry@garfieldtech.com), Arnaud Le Blanc (arnaud.lb@gmail.com) * Status: In Discussion * First Published at: https://wiki.php.net/rfc/partial_function_application_v2 * Implementation: https://github.com/arnaud-lb/php-src/pull/12 ===== Introduction ===== Partial Function Application (PFA) refers to the process of calling a function with only some of its required parameters, delaying the rest to a later time. It is logically equivalent to the following: function f(int $a, int $b, int $c) {} $partialF = fn(int $b) => f(1, $b, 3); However, using an arrow function is often cumbersome, as it requires manually replicating all the parameter information. That is unnecessary work both for the author and reader. That is especially relevant when dealing with callbacks or the new pipe operator. For example: $result = array_map(static fn(string $string): string => str_replace('hello', 'hi', $string), $arr); $foo |> static fn(array $arr) => array_map(static fn(string $string): string => str_replace('hello', 'hi', $string), $arr) ; While the examples above show "all the trimmings," including technically optional syntax, it is usually recommended to include type information, and many static analysis tools will require it. Even without the optional parts, there is still a lot of unnecessary and cumbersome indirection in the code. With PFA as proposed here, the above examples could be simplified to: $result = array_map(str_replace('hello', 'hi', ?), $arr); $foo |> array_map(str_replace('hello', 'hi', ?), ?) ; That would produce the same result, but be vastly more ergonomic for all involved. PFA is a core feature of a number of functional languages. While integrating it as a concept as deeply as, say, Haskell does is not feasible in PHP, the ergonomic benefits of PFA make it a key component of PHP's growing functional capabilities. From one point of view, PFA is a natural extension of [[rfc:first_class_callable_syntax|First-class callable syntax]] (FCC), added in PHP 8.1. Conversely, FCC is the degenerate case of PFA. The original FCC RFC was presented as a "first step" junior version of PFA, based on the syntax proposed by the [[rfc:partial_function_application|Partial Function Application v1]] RFC. FCC has clearly demonstrated its value in code simplification over the last few years, and PFA makes it even more powerful. This RFC is substantially similar to the previous PFA RFC, though there are a few detail changes. The implementation also leverages the existing work of FCC. Note that for the purposes of this RFC, "function" refers to any callable. Named function, named method, named static method, anonymous function, short-anonymous function, invokable object, etc. Partial application applies to all of them. ===== Proposal ===== ==== Overview ==== Partial Application consists of a function call in which one or more arguments is replaced with ? (question mark) or ... (an ellipsis) . If that is found, then rather than calling the function the engine will create a Closure which stores the function and the arguments that were provided and return that. The Closure signature will be created to match the signature of the un-specified parameters. That means the closure can be inspected via reflection and will display the full type information for both parameters and the return value derived from the underlying function. For example, the following pairs are logically equivalent (with one caveat noted below regarding ''func_get_args()''): // Given function foo(int $a, int $b, int $c, int $d): int { return $a + $b + $c + $d; } $f = foo(1, ?, 3, 4); $f = static fn(int $b): int => foo(1, $b, 3, 4); $f = foo(1, ?, 3, ?); $f = static fn(int $b, int $d): int => foo(1, $b, 3, $d); $f = foo(1, ...); $f = static fn(int $b, int $c, int $d): int => foo(1, $b, $c, $d); $f = foo(1, 2, ...); $f = static fn(int $c, int $d): int => foo(1, 2, $c, $d); $f = foo(1, ?, 3, ...); $f = static fn(int $b, int $d): int => foo(1, $b, 3, $d); A partialed Closure may be partially applied again. In that case, the engine detects that it is a partial Closure and simply fills in additional arguments as appropriate, rather than creating another layer of wrapping Closure. ==== Placeholder Semantics ==== This RFC introduces two placeholder symbols: * The argument place holder ''?'' means that exactly one argument is expected at this position. * The variadic place holder ''...'' means that zero or more arguments may be supplied at this position. The syntax for a partial function application consists several parts, in the following order: * Zero or more positional values (literals or variables) mixed with ''?''. * Zero or one ''...'' symbol, which indicates "Any arguments not already specified." * Zero or more named arguments with values OR placeholders. All of these will get mapped to their original position in the underlying function and behave as though they had been positional. This behavior is identical to how named arguments operate already. Parameters in the resulting Closure will inherit the following aspects directly from the underlying function: * Name * Type * Optionality * Default value * pass-by-ref or not If the function is variadic, there are two additional rules: * Any positional placeholders that run into the variadic portion become required. * If any positional placeholders run into the variadic portion, all prior remaining placeholders become required. However, those parameters may not be called with named arguments, as there is no name to use. The name and order of parameters in the resulting closure will always match the order of the original function. Partial application cannot change that, although it supports providing values in multiple orders. Using the ''...'' indicator when there are no remaining parameters is legal, and will result in a Closure with no parameters that will call the underlying function with the values provided. Effectively, it becomes a way to delay execution of a function call. This technique is known in CS circles by the delightful name of "a thunk." While in theory that means a call like this would be legal: function stuff(int $i, string $s, float $f, Point $p, int $m = 0) {} $c = stuff(1, ?, ..., p: ?, f: 3.14); In practice, we anticipate most uses to fall into one of three styles: // Provide all but one argument. $c = array_map(strtoupper(...), ?); $c = array_filter(?, is_numeric(...)); // Provide one or two values to start with, and "the rest". $c = stuff(1, 'two', ...); // Fill in a few parameters by name, and leave the rest as is. $c = stuff(..., f: 3.14, s: 'two'); ==== Examples ==== It is easier to describe the semantics by example. Each of the sets of statements below contain logically equivalent statements. The result of each is the creation of a callable named ''"$c''. (Note the caveat below regarding ''func_get_args()''.) //// Regular functions //// // Given: function stuff(int $i, string $s, float $f, Point $p, int $m = 0): string {} // Manually specify all placeholders. $c = stuff(?, ?, ?, ?, ?); // Manually specify the first two values, and pull the rest "as is". $c = stuff(?, ?, ...); $c = fn(int $i, string $s, float $f, Point $p, int $m = 0): string => stuff($i, $s, $f, $p, $m); // The degenerate "first class callables" case. (Supported since 8.1) $c = stuff(...); $c = fn(int $i, string $s, float $f, Point $p, int $m = 0): string => stuff($i, $s, $f, $p, $m); // Provide some values, require the rest to be provided later. $c = stuff(1, 'hi', ?, ?, ?); $c = stuff(1, 'hi', ...); $c = fn(float $f, Point $p, int $m = 0): string => stuff(1, 'hi', $f, $p, $m); // Provide some values, but not just from the left. $c = stuff(1, ?, 3.5, ?, ?); $c = stuff(1, ?, 3.5, ...); $c = fn(string $s, Point $p, int $m = 0): string => stuff(1, $s, 3.5, $p, $m); // Provide just the last value. $c = stuff(?, ?, ?, ?, 5); $c = fn(int $i, string $s, float $f, Point $p): string => stuff($i, $s, $f, $p, 5); // Not accounting for an optional argument // means it will always get its default value. $c = stuff(?, ?, ?, ?); $c = fn(int $i, string $s, float $f, Point $p): string => stuff($i, $s, $f, $p); // Named arguments can be pulled "out of order", and still work. $c = stuff(?, ?, f: 3.5, p: $point); $c = stuff(?, ?, p: $point, f: 3.5); $c = fn(int $i, string $s): string => stuff($i, $s, 3.5, $point); // The ... "everything else" placeholder respects named arguments. $c = stuff(?, ?, ..., f: 3.5, p: $point); $c = fn(int $i, string $s, int $m = 0): string => stuff($i, $s, 3.5, $point, $m); // Prefill all parameters, making a "delayed call" or "thunk" $c = stuff(1, 'hi', 3.4, $point, 5, ...); $c = fn(...$args): string => stuff(1, 'hi', 3.4, $point, 5, ...$args); // Placeholders may be named, too. Their order doesn't // matter as long as they come after the ..., if any. $c = stuff(?, p: $point, f: ?, s: ?, m: 4); $c = stuff(..., m: 4, p: $point, i: ?); $c = fn(int $i, string $s, float $f): string => stuff($i, $s, $f, $point, 4); //// Variadics //// // Given function things(int $i, ?float $f = null, Point ...$points) { ... } // FCC equivalent. The signature is unchanged. $c = things(...); $c = fn(int $i, ?float $f = null, Point ...$points): string => things(...[$i, $f, ...$points]); // Provide some values, but allow the variadic to remain variadic. $c = things(1, 3.14, ...); $c = fn(Point ...$points): string => things(...[1, 3.14, ...$points]); // In this version, the partial requires precisely four arguments, // the last two of which will get received // by things() in the variadic parameter. // Note too that $f becomes required in this case. $c = things(?, ?, ?, ?); $c = fn(int $i, ?float $f, Point $p1, Point $p2): string => things($i, $f, $p1, $p2); //// Esoteric examples unlikely to ever bee seen in the wild. //// function four(int $a, int $b, int $c, int $d) { print "$a, $b, $c, $d\n"; } // These all print "1, 2, 3, 4" (four(...))(1, 2, 3, 4); (four(1, 2, ...))(3, 4); (four(1, 2, 3, ?))(4); (four(1, ?, ?, 4))(2,3); (four(1, 2, 3, 4, ...))(); (four(..., d: 4, a: 1))(2, 3); (four(c: ?, d: 4, b: ?, a: 1))(2, 3); //// Other callable styles //// // Given: class E { public function __construct(private int $x, private int $y) {} public static function make(int $x, int $y): self; public function foo(int $a, int $b, int $c): int {} } // This is a syntax error: $e = new E(1, ?); // This is allowed: $eMaker = E::make(1, ?); $eMaker = fn(int y): E => E::make(1, $y); // Fills in the remaining values and runs the method. $e = $eMaker(2); $c = $e->foo(?, ?, 3, 4); $c = fn(int $a, int $b): int => $e->foo($a, $b, 3, 4); // $c can then be further refined, but the extra layer is removed: $c2 = $c(1, ?); $c2 = fn(int $b): int => $e->foo(1, $b, 3, 4); // Given: class RunMe { public function __invoke(int $a, int $b): string {} } $r = new RunMe(); $c = $r(?, 3); $c = fn(int $a): string => $r($a, 3); ==== Error examples ==== The following examples are all errors, for the reasons given. // Given function stuff(int $i, string $s, float $f, Point $p, int $m = 0) {} // throws Error(not enough arguments and/or place holders for application of stuff) $c = stuff(?); // throws Error(too many arguments and/or place holders for application of stuff) $c = stuff(?, ?, ?, ?, ?, ?); // throws Error(Named parameter $i overwrites previous place holder) $c = stuff(?, ?, 3.5, $point, i: 5); // Fatal error: Named arguments must come after all place holders $c = stuff(i:1, ?, ?, ?, ?); // Fatal error: Named arguments must come after all place holders $c = stuff(?, ?, ?, p: $point, ?); ==== func_get_args() and friends ==== ''func_get_args()'', ''func_num_args()'', and similar functions are unaware of intermediate applications: The underlying function is only ever called once, using all the parameters that were built up over any number of partial applications. That means they will behave exactly as though all of the specified arguments were passed directly to the function all at once. function f($a = 0, $b = 0, $c = 3, $d = 4) { echo func_num_args() . PHP_EOL; var_dump($a, $b, $c, $d); } f(1, 2); $f = f(?, ?); $f(1, 2); Would output: 2 int(1) int(2) int(3) int(4) 2 int(1) int(2) int(3) int(4) There is one small inconsistency in behavior when using a partial with a variadic placeholder. In the following case, the function is non-variadic but it is partialed using a variadic placeholder. That results in a variadic Closure, which will pass all of its arguments to the underlying function. function foo(int $a, int $b, int $c) {} // These two are equivalent. $f = foo(?, ?, ?); $f = fn(int $a, int $b, int $c) => foo($a, $b, $c); // Whereas strictly speaking, a variadic placeholder is equivalent to this: $f = foo(1, ...); $f = fn(int $b, int $c): int => foo(1, $b, $c, $d, ...array_slice(func_get_args(), 3)); // And thus calling it like this: $f(2, 3, 4, 5, 6); // would result in: foo(1, 2, 3, 4, 5, 6); User space functions by default ignore extraneous arguments, //except// for ''func_get_args()'', and ''func_num_args()'', which would recognize them. PHP internal functions by default will error on extraneous arguments. This behavior already exists today for first class callables, and to our knowledge has never caused an issue. We therefore expect it to continue to be a hypothetical edge case of minimal impact, but we include it for completeness. ==== Evaluation order ==== One subtle difference between the existing short lambda syntax and the partial application syntax is that argument expressions are evaluated in advance. That is: function getArg() { print __FUNCTION__ . PHP_EOL; return 'hi'; } function speak(string $who, string $msg) { printf("%s: %s\n", $who, $msg); } $arrow = fn($who) => speak($who, getArg()); print "Arnaud\n"; $arrow('Larry'); /* Prints: Arnaud getArg Larry: hi */ $partial = speak(?, getArg()); print "Arnaud\n"; $partial('Larry'); /* Prints: getArg Arnaud Larry: hi */ The reason is that in the partial application case, the arguments are all evaluated first, and then the engine detects that some have placeholders. In the short lambda case, the closure object is created first around an expression body that just so happens to include a function call that will happen later. ==== Magic methods ==== Magic methods ''%%__call%%'' and ''%%__callStatic%%'' are supported. Specifically, creating a partial Callable off of a magic method will result in a callable with a signature consisting the number of arguments specified in the partial call, all with no type and named ''%%$args%%'' in reflection. Put another way, a magic call method has the implicit signature ''%%function (...$args)%%'' and when partialed will behave as any other function with that signature. Named arguments are only supported on the variadic portion of the partial function's signature, as there are no underlying names to leverage. For example: class Foo { public function __call($method, $args) { printf("%s::%s\n", __CLASS__, $method); print_r($args); } } $f = new Foo(); $m = $f->method(?, ?); $m(1, 2); /* Prints: Foo::method Array ( [0] => 1 [1] => 2 ) */ $m(a: 1, b: 2); /* Prints: Foo::method Array ( [a] => 1 [b] => 2 ) */ // This will error with "Unknown named parameter $a" $m->method(?)(a: 1); // But this is acceptable. $m->method(...)(a: 1); The ''%%__get%%'' and ''%%__set%%'' magic methods are not called as methods, and thus there is no way to partially apply them. ==== Constructors ==== Because constructors are invoked in an indirect way by the engine, supporting them is notably more work than other functions. Additionally, the use cases for partially applying a constructor are few, especially now that we have lazy objects (as of PHP 8.4). For that reason, partial application is not supported for ''new'' calls. (Static factory methods are fully supported, of course.) ==== Implementation notes and optimizations ==== As with First-Class Callables, partial application cannot generally be implemented at compile time. The compiler cannot reliably detect what function is being partially applied or invoked in all cases. That means at least some work must be delayed until runtime, when the function is known. Partial Application builds on the existing mechanisms and opcodes introduced with First-Class Callables. Specifically partial application uses the VM call stack to temporarily store information such as the function being applied as well as arguments and placeholders. A Closure is then created out of this information and the call frame is discarded. Opcodes that are normally used for function calls are reused in this context to minimize code duplication and ensure a consistent behavior. If the function being called is already a partial closure, a new closure will be created that merges both the previous and new arguments into one list rather than creating nested objects. This process allows reusing the existing function resolution logic, inline caches, error handling, parameter passing logic, etc. That keeps the behavior of partials consistent with normal function calls with little additional effort. In testing, we have found that creating a closure in this fashion is slightly slower than just creating one manually with an arrow function. Invoking the resulting closure is approximately the same as invoking a manually-created closure. On the whole, we expect the performance difference to be negligible in practice in most situations. There is one case which can be optimized at compile time, however. If a PFA call is placed directly as the right-side of a pipe operator, and it can be statically determined to take only a single argument, then the partial application is optimized away and turned into a direct call statement, the same as first-class callables are. As that is one of the two primary expected use cases for partial application (the other being a callback to another function), that should eliminate the majority of the overhead in practice. Specifically, the following forms can be optimized away when used with a pipe operator: * ''foo(?)'' * ''foo(1, ?)'' * ''foo(1, a: ?)'' * ''foo(1, ...)'' These cases cannot be optimized away, and will form a closure as normal. * ''foo(..., a: 1)'' (Variadic + named argument, it's not clear at compile time where the 'a' goes.) * ''foo(?, ?)'', ''foo(?, a: ?)'', ''foo(?, ...)'' (Multi-param function, so the compiler doesn't know where the piped value would go. Also, these would be incompatible with a pipe anyway unless one of those placeholders is optional, in which case there's no point in specifying it at all.) ===== Common use cases ===== Although partial application has a wide range of use cases, in practice we anticipate there to be three general categories that will be the overwhelming majority cases: ==== Callable reference ==== First class support for creating a Closure from a callable with ''...''. This is already provided by First Class Callables, as a "junior version" of the syntax presented here, and their use is already quite common. ==== Unary functions ==== A unary function is a function with a single parameter. Many callbacks require a unary function, which partial application makes trivial to produce. Similarly, the pipe operator requires a unary function on its right-hand side, which partial application provides. For example: $result = array_map(in_array(?, $legal, strict: true), $input); ==== Delayed execution ==== Partial application may return a closure with all required arguments applied, followed by ''...'': That results in a closure that has all the arguments it needs for the underlying function but is not, yet, executed, and takes zero or more arguments. It may therefore be called to execute the original function with its parameters at a later time. Such a parameter-less delayed function goes by the delightful name "thunk" in the literature. function expensive(int $a, int $b, Point $c) { /* ... */ } $default = expensive(3, 4, $point, ...); // $default here is a closure object. // expensive() has not been called. // Some time later, evaluate the function call only when necessary. if ($some_condition) { $result = $default(); } ==== Reflection ==== Because a partial application results in a ''Closure'', no changes to the reflection API are necessary. It may be used by reflection in the same fashion as any other Closure or function, specifically using ''ReflectionFunction''. One additional method has been added to ReflectionFunctionAbstract: public function ReflectionFunctionAbstract::isPartial() : bool; ===== Backward Incompatible Changes ===== None. Only new syntax is enabled. ===== Proposed PHP Version(s) ===== 8.5 or 8.6 ===== RFC Impact ===== ===== Proposed Voting Choices ===== * Yes * No ===== Patches and Tests ===== Links to any external patches and tests go here. If there is no patch, make it clear who will create a patch, or whether a volunteer to help with implementation is needed. Make it clear if the patch is intended to be the final patch, or is just a prototype. For changes affecting the core language, you should also provide a patch for the language specification. ===== Implementation ===== After the project is implemented, this section should contain - the version(s) it was merged into - a link to the git commit(s) - a link to the PHP manual entry for the feature - a link to the language specification section (if any) ===== References ===== Links to external references, discussions or RFCs ===== Rejected Features ===== Keep this updated with features that were discussed on the mail lists.