====== 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: Draft * First Published at: https://wiki.php.net/rfc/partial_function_application_v2 ===== 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, 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 ''?'' or ''...''. 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 function. For example, the following pairs are logically equivalent: // 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 rules for applying them are as follows: * A positional value is stored and will be passed to the underlying function when the partial is invoked. * A ''?'' placeholder will copy the parameter definition at the same position from the underlying function. That includes type, optionalness, and whether it accepts a value by reference. * ''...'' may only occur zero or one time. If it appears, then it means all * The following rules apply to partial application: * ''...'' may only occur zero or one time * ''...'' may only be followed by named arguments or named placeholders * named placeholders must come after all positional placeholders * named arguments must come after all place holders The 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. * The type of parameters in the resulting closure will always match the type in the original function. * Whether a parameter is by-reference or by-value in the resulting closure will always match the original function. * A ''?'' placeholder will create a required value, regardless of whether the underlying value is required. For positional arguments, either a value may be provided OR a single ''?'' placeholder. If specified, the ''...'' placeholder means "any additional parameters not otherwise specified." Named placeholders come next, and may only be used with parameters not already covered by positional placeholders or arguments. Finally, named arguments come last, and provide values for arguments by name. 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 lines below contain logically equivalent statements. The result of each is the creation of a callable named ''"$c''. // Given: function stuff(int $i, string $s, float $f, Point $p, int $m = 0) {} // Manually specify all placeholders. $c = stuff(?, ?, ?, ?, ?); $c = fn(int $i, string $s, float $f, Point $p, int $m) => stuff($i, $s, $f, $p, $m); // Manually require the first two values, and pull the rest "as is". // This differs from Ex 1 because the ... // retains the optionalness of $m. $c = stuff(?, ?, ...); $c = stuff(...); // In this case, it is equivalent to FCC. $c = fn(int $i, string $s, float $f, Point $p, int $m = 0) => stuff($i, $s, $f, $p, $m); // Provide a single value, require the rest to be provided later. $c = stuff(1, 'hi', ?, ?, ?); $c = fn(float $f, Point $p, int $m) => stuff(1, 'hi', $f, $p, $m); // Ex 4 $c = stuff(1, 'hi', ...); $c = fn(float $f, Point $p, int $m = 0) => stuff(1, 'hi', $f, $p, $m); // Ex 5 $c = stuff(1, ?, 3.5, ?, ?); $c = fn(string $s, Point $p, int $m) => stuff(1, $s, 3.5, $p, $m); // Ex 6 $c = stuff(1, ?, 3.5, ...); $c = fn(string $s, Point $p, int $m = 0) => stuff(1, $s, 3.5, $p, $m); // Ex 7 $c = stuff(?, ?, ?, ?, 5); $c = fn(int $i, string $s, float $f, Point $p) => stuff($i, $s, $f, $p, 5); // Ex 8 // 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) => stuff($i, $s, $f, $p); // Ex 9 $c = stuff(?, ?, f: 3.5, p: $point); $c = stuff(?, ?, p: $point, f: 3.5); $c = fn(int $i, string $s) => stuff($i, $s, 3.5, $point); // Ex 10 $c = stuff(?, ?, ..., f: 3.5, p: $point); $c = fn(int $i, string $s, int $m = 0) => stuff($i, $s, 3.5, $point, $m); // Ex 11 // Prefill all params, making a "delayed call" $c = stuff(1, 'hi', 3.4, $point, 5, ...); $c = fn(...$args) => stuff(1, 'hi', 3.4, $point, 5, ...$args); // Ex 12 $c = stuff(?, ?, ?, ..., p: $point); $c = fn(int $i, string $s, float $f, ...$args) => stuff($i, $s, $f, $point, ...$args); // For a function with a variadic argument, the // variadic-ness is not propagated to the partial directly. // It may, however, be implicitly handled by ''...'' function things(int $i, float $f, Point ...$points) { ... } // Ex 13 $c = things(...); $c = fn(int $i, float $f, ...$args) => things(...[$i, $f, ...$args]); // Ex 14 $c = things(1, 3.14, ...); $c = fn(...$args) => things(...[1, 3.14, ...$args]); // Ex 15 // In this version, the partial requires precisely four arguments, // the last two of which will get received // by things() in the variadic parameter. $c = things(?, ?, ?, ?); $c = fn(int $i, float $f, Point $p1, Point $p2) => things($i, $f, $p1, $p2); function four(int $a, int $b, int $c, int $d) { print "$a, $b, $c, $d\n"; } // Ex 16 // 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); // Ex 17 function zero() { print "hello\n"; } zero(...)(); // prints "hello\n" ==== 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, ?, ?, ?, ?); // Cannot use placeholder on named arguments. // Parse error: syntax error, unexpected token "?" $c = stuff(1, ?, 3.5, p: ?); // 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) ==== 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. Named arguments are also supported, the same as with ''%%__call%%'' natively, even though the name won't match reflection. 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 ) */ The ''%%__get%%'' and ''%%__set%%'' magic methods are not called as methods, and thus there is no way to partially apply them. ==== Implementation notes and optimizations ==== As with First-Class Callables, partial application cannot generally be implemented at compile time. The compiler cannot reliably detect all cases where a partial application may occur. Instead, the function call process will detect a partial application when it sees either ''?'' or ''...''. It will then "back out" of the call and convert it to a Closure instead. If the function being called is already a partial closure, it will not be modified but simply updated with additional saved arguments as appropriate. In testing, we have found that creating a closure in this fashion is slightly slower than just creating one manually with an arrow function. However, invoking the resulting closure is slightly faster than invoking a manually-created closure. On the whole, we expect performance to be about break-even in most cases. 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. ===== 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) ===== List the proposed PHP versions that the feature will be included in. Use relative versions such as "next PHP 8.x" or "next PHP 8.x.y". ===== RFC Impact ===== ==== To SAPIs ==== Describe the impact to CLI, Development web server, embedded PHP etc. ==== To Existing Extensions ==== Will existing extensions be affected? ==== To Opcache ==== It is necessary to develop RFC's with opcache in mind, since opcache is a core extension distributed with PHP. Please explain how you have verified your RFC's compatibility with opcache. ===== Open Issues ===== Make sure there are no open issues when the vote starts! ===== Future Scope ===== This section details areas where the feature might be improved in future, but that are not currently proposed in this RFC. ===== Proposed Voting Choices ===== Include these so readers know where you are heading and can discuss the proposed voting options. ===== 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.