rfc:partial_function_application_v2

PHP 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 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 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);

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 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.
  • Zero or one ... symbol, which indicates “Any arguments not already specified.”

Parameters and the return value in the resulting Closure will inherit the following aspects directly from the underlying function:

  • Name
  • Type
  • Optionality
  • Default value
  • pass-by-ref or not
  • The presence of the #[NoDiscard] and #[SensitiveParameter] attributes (but only these)

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.
  • Positional placeholders that map to the variadic portion of the underlying function will be named for the variadic argument name, with a 0-based numeric suffix. So int ...$nums will translate to $nums0, $nums1, etc., and may be called by name if desired. If a parameter by that name already exists, it will be skipped over.

For example:

function foo(int $a = 5, int $b = 1, string ...$c) { }
$pfa = foo(?, ?, ?, ?);
 
// Equivalent to this:
// Note that $a and $b become required, because there must be at least 4 arguments.
$pfa = fn(int $a, int $b, string $c0, string $c1) => foo($a, $b, $c0, $c1);

The reason for including only those two attributes is that they have direct runtime implications. #[SensitiveParameter] is a security concern, so must be retained. #[NoDiscard] for the return value logically makes sense to pass-through, as the intermediary closure PFA creates (see below) will always pass on the return value of the underlying function. In contrast, copying #[Deprecated] would result in two warnings being issued instead of one (which is pointless), and #[Override] doesn't make sense on a closure. It is impossible to tell if user-defined attributes logically make sense to copy, as the engine won't have the context to know. We therefore opt to ignore them.

This is in contrast to FCC, which does retain all attributes. However, FCC does not create a full interstitial closure the way PFA does; it just creates a special reference to the underlying function.

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 required 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, ...);
 
// Equivalent to:
$c = fn(string $s, Point $p, int $m = 0) => stuff(1, $s, 3.14, $p, $m);

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 $i1, string $s2, float $f3, Point $p4, int $m5 = 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 $i1, string $s2, float $f3, Point $p4, int $m5 = 0): string
  => stuff($i1, $s2, $f3, $p4, $m5);
 
// The degenerate "first class callables" case. (Supported since 8.1)
$c = stuff(...);
$c = fn(int $i1, string $s2, float $f3, Point $p4, int $m5 = 0): string
  => stuff($i1, $s2, $f3, $p4, $m5);
 
// Provide some values, require the rest to be provided later.
$c = stuff(1, 'hi', ?, ?, ?);
$c = stuff(1, 'hi', ...);
$c = fn(float $f3, Point $p4, int $m5 = 0): string => stuff(1, 'hi', $f3, $p4, $m5);
 
// Provide some values, but not just from the left.
$c = stuff(1, ?, 3.5, ?, ?);
$c = stuff(1, ?, 3.5, ...);
$c = fn(string $s2, Point $p4, int $m5 = 0): string => stuff(1, $s2, 3.5, $p4, $m5);
 
// Provide just the last value.
$c = stuff(?, ?, ?, ?, 5);
$c = fn(int $i1, string $s2, float $f3, Point $p4): string 
  => stuff($i1, $s2, $f3, $p4, 5);
 
// Not accounting for an optional argument
// means it will always get its default value.
$c = stuff(?, ?, ?, ?); 
$c = fn(int $i1, string $s2, float $f3, Point $p4): string
  => stuff($i1, $s2, $f3, $p4);
 
// Named arguments can be pulled "out of order", and still work.
$c = stuff(?, ?, f3: 3.5, p4: $point);
$c = stuff(?, ?, p4: $point, f3: 3.5);
$c = fn(int $i1, string $s2): string => stuff($i1, $s2, 3.5, $point);
 
// The ... "everything else" placeholder respects named arguments.
$c = stuff(?, ?, f3: 3.5, p4: $point, ...);
$c = fn(int $i1, string $s2, int $m5 = 0): string => stuff($i1, $s2, 3.5, $point, $m5);
 
// Prefill all parameters, making a "delayed call" or "thunk"
$c = stuff(1, 'hi', 3.4, $point, 5, ...);
$c = fn(): string => stuff(1, 'hi', 3.4, $point, 5);
 
// Placeholders may be named, too.  Their order doesn't matter.
$c = stuff(?, p4: $point, f3: ?, s: ?, m5: 4);
$c = stuff(m5: 4, p4: $point, i: ?, ...);
$c = fn(int $i1, string $s2, float $f3): string => stuff($i1, $s2, $f3, $point, 4);
//// Variadics ////
 
// Given
function things(int $i1, ?float $f3 = null, Point ...$points) { ... }
 
// FCC equivalent.  The signature is unchanged.
$c = things(...);
$c = fn(int $i1, ?float $f3 = null, Point ...$points): string => things(...[$i1, $f3, ...$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 $i1, ?float $f3, Point $points0, Point $points1): string => things($i1, $f3, $points0, $points1);
 
//// 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 will generate a compile error: "Cannot create Closure for new expression."
$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:
$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 ArgumentCountError: Partial application of stuff() expects at least 4 arguments, 1 given
$c = stuff(?);
 
// throws ArgumentCountError: Partial application of stuff() expects at most 5 arguments, 6 given
$c = stuff(?, ?, ?, ?, ?, ?);
 
// throws Error(Named parameter $i overwrites previous placeholder)
$c = stuff(?, ?, 3.5, $point, i: 5);
 
// Fatal error: "Cannot use positional placeholder after named argument."
$c = stuff(i: 1, ?, ?, ?, ?);
 
// Fatal error: "Cannot use positional placeholder after named argument."
$c = stuff(?, ?, ?, p: $point, ?);

Incompatible functions

A select number of PHP built-in functions that operate on a language level are not compatible with partial application. The same is true of FCC, although for a slightly different reason. In the case of FCC, it's because the functions cannot be called dynamically and FCC only does dynamic calls.

(function ($f) { $f(); })("func_get_args"); 
 
// Error" Cannot call func_get_args() dynamically

For PFA, the issue is that all of these functions are designed to operate on a specific context, and the whole point of PFA is to change that context. So while they technically could be made to execute, their result would be at best meaningless.

The list of disallowed functions is the same for PFA as for FCC. The most notable cases include:

  • compact()
  • extract()
  • func_get_arg()
  • get_defined_vars()

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 engine will pre-bind the values to the closure when creating it. 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, magic methods are treated as a method with the implicit signature function (...$args), and will be partialed the same as any other function with that signature. As with variadics described above, required parameters will be created without type and with generated names $args0, $args1, etc.

For example:

class Foo {
    public function __call($method, $args) {
        printf("%s::%s\n", __CLASS__, $method);
        print_r($args);
    }
}
 
$f = new Foo();
$m = $f->method(?, ?);
// Equivalent to:
$m = fn($args0, $args1) => $f->method($args0, $args1);
 
 
$m(1, 2);
 
/* Prints:
Foo::method
Array
(
    [0] => 1
    [1] => 2
)
*/
 
$m(args0: 1, args1: 2);
 
/* Prints:
Foo::method
Array
(
    [args0] => 1
    [args1] => 2
)
*/
 
// This will error with "Unknown named parameter $a"
$m(a: 1, b: 2);
 
// 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.)

Constant expressions

A PFA call may be included in a constant expression, provided its arguments are all themselves constant expressions. The constraints are the same as for First-Class-Callable constant expressions.

class Foo
{
    public function __construct(
        private \Closure $callback = bar(1, 2, ?),
    ) {}
}

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.

Instead, when the engine hits a PFA call it generates the AST of an equivalent closure, then compiles and executes that. The secondarily-compiled AST is stored in the opcache, so only needs to happen once.

Examples:

// Given
function foo(int $a, int $b, int $c): string { return "$a, $b, $c\n"; }
 
// This code:
$f = foo(?, 3, ?);
 
// Transpiles into:
$f = function (int $a, int $c): string {
    return foo($a, 3, $c);
};
 
// And this code:
$f = function ($a, $b) {};
$g = $f($a, ?);
 
// Transpiles into:
$g = function ($b) use ($a, $f) {
    return $f($a, $b);
};

As shown, values required by a PFA are passed to the generated closure via “used variables”. These used variables are exposed via ReflectionFunctionAbstract::getClosureUsedVariables(), just as they would be in a manually-written long-closure.

We consider the use-based binding to be an implementation detail, but this is how it currently works. It should not be considered a guaranteed part of the API.

The result is that a PFA closure will behave identically to a closure as far as debugging output, optimization, reflection, stack traces, and so forth.

The performance characteristics of a closure created this way are essentially the same as the equivalent literal closure.

Pipe operator

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.)

Variadics, func_get_args(), and extraneous arguments

The transpilation approach described above has implications for how variadics, func_get_args(), and extra trailing arguments are handled. In general, they are designed to be the most “obvious” behavior given the transpilation equivalency above, and to mirror First Class Callable behavior where relevant.

If a PFA call has no ... placeholder, then any extraneous arguments to the resulting closure will be ignored. That is consistent with how manually writing the equivalent closure would behave, and is the same regardless of whether the underlying function is variadic.

function foo(int $i, int $j) {
    echo func_num_args() . PHP_EOL;
}
 
$c = foo(1, ?);
$c = function (int $j) { return foo(1, $j); }
 
// The extra parameter here will be passed to the 
// closure object, which will simply ignore it.
$c(4, 'ignore me'):
// Outputs 2

If a PFA call has a ... placeholder, then any extraneous arguments will be passed through to the underlying function. Whether the underlying function cares or not is up to the function. However, func_get_args(), func_num_args(), and similar functions will “see” the extra arguments, as though they were passed directly. This is the same behavior as First Class Callables has today. (The extra arguments are not collected with a variadic so as to avoid the function appearing variadic in Reflection or in repeated partial application.)

function foo(int $i, int $j) {
    echo func_num_args() . PHP_EOL;
}
 
$c = foo(1, ?, ...);
$c = function (int $j) { return foo(1, $j, ...array_slice(func_get_args(), 1)); }
 
// The extra parameter here will be passed to the 
// closure object, which will forward it directly to the
// underlying function.  It will be accessible only 
// via ''func_get_args()'' et al.
$c(4, 'ignore me'):
// Prints 3

If a PFA call has a ... placeholder and the underlying function is variadic, then the trailing arguments will be forwarded directly but will get “collected” by the variadic parameter as normal. In this case, the partial closure will appear as variadic in Reflection.

function foo(int $i, int $j, ...$extra) {
    echo func_num_args() . PHP_EOL;
}
 
$c = foo(1, ?, ...);
$c = function (int $j, ...$args) { return foo(1, $j, ...$args); }
 
// The extra parameter here will be passed to the 
// closure object, which will forward it directly to the
// underlying function.  It will show up as part of the
// $extra array.
$c(4, 'ignore me'):
// Prints 3

Note that non-variadic internals functions will error if passed extraneous arguments rather than ignoring them. That is pre-existing behavior and is unaffected by this RFC.

Caution with extraneous arguments and optional parameters

As noted, PHP user-space functions will normally silently accept and ignore extraneous trailing arguments. Most of the time that is convenient, but with PFA there is one case in which that becomes quite dangerous.

Consider:

$firstNonZero = array_find($arr, intval(?));

array_find()'s callback will be passed both the array value and key, in that order. The assumption is that a callback that only cares about the value will drop the key and all will be fine. However, intval() takes a second optional $base parameter. The key from the array almost certainly makes no sense whatsoever as an argument to it.

While this example is rather contrived, the same issue would arise for any function that has optional arguments if extra arguments passed through the PFA closure.

That means care must be taken when using a variadic placeholder with a function that contains optional parameters. Those parameters will be exposed in the resulting closure, which if it is used as a callback may result in unexpected behavior. A PFA that has no variadic placeholder eliminates this risk.

That is:

$inter = intval(?);
$inter('5'); // Works fine, the default $base is used.
$inter('5', '6'); // The '6' is completely ignored and never reaches intval().
 
// Because it is equivalent to:
$inter = fn(mixed $value): int => intval($value);
 
$variInter = intval(...);
$variInter('5'); // Works fine, the default $base is used.
$variInter('5', '6'); // The '6' is passed through to intval()'s $base parameter.
 
// Because it is equivalent to:
$inter = fn(mixed $value, int $base = 10): int => intval($value, $base, ...array_slice(func_get_args(), 2));

Scoping

To be consistent with First Class Callables, PFA closures will be static (and thus unbindable) iff the function being partialed is static. If it has a bound object, then the closure may be rebound but only to the same type of object.

That means the following will all produce static closures:

$f = foo(?, ?);
$fb = Foo::bar(?, ?); // (assuming bar() is a static method)
$b = $f(1, ?);
$c = foo(...)(?);

PFA closures on an object method will behave like so:

class C {
    function f($a) {
        return self::class;
    }
}
class CSubClass extends C {
    function g($a) {
        return self::class;
    }
}
class Unrelated {}
 
$c = new C();
$f = $c->f(?);
$f = $f->bindTo(null, C::class); // Warning: Cannot unbind $this of method, this will be an error in PHP 9 (returns null)
$f = $f->bindTo($c, D::class); // Warning: Cannot rebind scope of closure created from method, this will be an error in PHP 9 (returns null)
$f = $f->bindTo(new CSubClass, C::class); // Allowed
$f = $f->bindTo(new Unrelated, C::class); // Warning: Cannot bind method C::{closure:/path/to/test.php:11}() to object of class Unrelated, this will be an error in PHP 9 (returns null)
 
$c = new CSubClass();
$c->f(?)(1); // string(1) "C"
$c->g(?)(1); // string(9) "CSubClass"

This behavior is slightly stricter than manually written closures, but is consistent with First Class Callables.

Debug output

To demonstrate how PHP will see a PFA closure:

// This code, if executed...
 
function f($a, #[SensitiveParameter] $b, $c) {
    g();
}
 
function g() {
    throw new Exception();
}
 
$f = f(?, ?, 3);
$f(1,2);
// Will produce output similar to the following:

Fatal error: Uncaught Exception in test.php:8
Stack trace:
#0 test.php(4): g()
#1 test.php(11): f(1, Object(SensitiveParameterValue), 3)
#2 test.php(12): {closure:/path/to/test.php:11}(1, Object(SensitiveParameterValue))
#3 {main}
  thrown in test.php on line 8

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.

Backward Incompatible Changes

None. Only new syntax is enabled.

Proposed PHP Version(s)

8.6

RFC Impact

Proposed Voting Choices

Implement $partial application as outlined in the RFC?
Real name Yes No Abstain
Final result: 0 0 0
This poll has been closed.

Implementation

After the project is implemented, this section should contain

  1. the version(s) it was merged into
  2. a link to the git commit(s)
  3. a link to the PHP manual entry for the feature
  4. 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.

rfc/partial_function_application_v2.txt · Last modified: by crell