rfc:partial_function_application

This is an old revision of the document!


PHP RFC: Partial Function Application

Introduction

Partial function application is the process of fixing (or binding) only some of the arguments to a function call and leaving the remainder to be bound a later point.

Proposal

Support partial application via the argument placeholder ?. Any function or method call in which one or more of the arguments is an argument placeholder results in a Partial being created instead of invoking the function or method.

The resulting Partial will have a prototype that matches the function being applied, with the bound arguments removed:

function whole($one, $two) {
    /* ... */
}
 
$partial = whole(?, 2);

$partial now has the prototype:

function($one) {
   /* ... */
}

When invoked, the Partial will call the bound function with the bound arguments at their bound positions, with the arguments to the partial being filled into the remaining spaces in-order:

function whole($one, $two) { /* ... */ }
 
// equivalent to calling whole(1, 2, 3)
$result = whole(?, 2)(1, 3);

Types

Type declarations are retained, as are parameter names (e.g. for reflection.)

function f(int $x, int $y): int {}
 
$partial = f(?, 42);

is equivalent to

$partial = function(int $x): int {
    return f($x, 42);
};

Variables/References

Variable arguments are used, and done so by reference if specified in the called function definition.

function f($value, &$ref) {}
 
$array = ['arg' => 0];
 
$f = f(?, $array['arg']);

is equivalent to

$ref = &$array['arg'];
$f = function($value) use (&$ref) {
    return f($value, $ref);
};

Optional Parameters

Optional parameters remain optional, inheriting defaults.

function f($a = 0, $b = 1) {}
 
$f = f(?, 2);

is equivalent to

$f = function($a = 0) {
    return f($a, 2);
};

func_num_args et al.

When a Partial invokes func_num_args, it shall behave as if the function was invoked directly, such that:

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)

Methods

The same logic applies to any callable, including an already partially applied function and to methods. For example:

class Action {
    public function volume(int $x, int $y, int $z): int {
        return $x * $y * $z;
    }
 
    static public function vol(int $x, int $y, int $z): int {
        return $x * $y * $z;
    }
}
 
$p1 = Action::vol(3, 5, ?);
print $p1(9); // Prints 135
 
$a = new Action();
$p2 = $a->volume(3, 5, ?);
print $p2(9); // Prints 135

Extra Arguments

You can pass as many additional arguments as you like when calling a function in PHP, beyond what's specified in the signature. When you pass extra arguments to the closure, or during partial application, they are passed along to the wrapped function.

function f($a, $b) {
    print_r(func_get_args());
}
 
$f = f(?, 2);

is actually akin to:

$f = function($a, ...$extraArgs) {
    return f($a, 2, ...$extraArgs);
};

(Though without unnecessary packing/unpacking, or $extraArgs appearing in reflection.)

So calling: $f(1, 3, 4) results in:

Array
(
    [0] => 1
    [1] => 2
    [2] => 3
    [3] => 4
)

Trailing Placeholders

At least one argument placeholder is necessary to indicate partial application; additional argument placeholders may be present but are unnecessary. This eases partial application of functions with long lists of optional parameters.

function f($m, $n, $o, $x = 0, $y = 0, $z = 0) {}
 
$f = f(1, ?);

The above snippet is roughly (minding the func_num_args section above) equivalent to:

$f = function($n, $o, $x = 0, $y = 0, $z = 0) {
    return f(1, $n, $o, $x, $y, $z);
};

As a side effect, that means that a callable of any arity can be partially applied without any binding with a single ?. That is:

class Foo {
  public function bar($a, $b, $c, $d, $e, $f, $g, $h): string { ... }
}
 
$f = new Foo();
$p = $f->bar(?);
 
// $p is now a partially applied function with the same 8 arguments
// as Foo::bar. Effectively there is no difference between now calling 
// $p(1, 2, 3, 4, 5, 6, 7, 8) and $foo->bar(1, 2, 3, 4, 5, 6, 7, 8).

That is especially useful when trying to use a method as a callable.

Extra Trailing Placeholders

Additional placeholders beyond a function's signature do not cause an error and have no additional impact, though effectively allow you to apply all arguments to a function to be called later.

$n = 1000000;
 
$log10ofn = log10($n, ?);

is equivalent to

$log10ofn = function() use ($n) {
    return log10($n);
}

Variadic Functions

Variadic functions retain their signatures. Placeholders in a variadic position remain optional (there are no defaults, places not filled are simply not included in what's sent to the underlying function.) Fixed and placeholder arguments can be interleaved together.

function f(...$args) {
    print_r($args);
}
 
$f = f(?, 2, ?, 4, ?, 6);
$f(1, 3);

Would output:

Array
(
    [0] => 1
    [1] => 2
    [2] => 3
    [3] => 4
    [4] => 6
)

Named arguments

Named arguments may be used when creating a partial, or when calling it later. The parameter names inherit to the partialed function. However, regardless of the order the arguments were given when creating the partial the order in the partial is the same as the original function.

That is, in the following example the function (message) is partially applied using named arguments out of order, but the resulting partial function ($p) still has its parameters in the original order. It may, of course, be called with named arguments itself if desired.

function message(string $salutation, string $name, string $stmt): string {
    return "$salutation, $name. $stmt" . PHP_EOL;
}
 
$p = message(stmt: ?, name: ?, salutation: "Hello");
// Both of these have the same effect.
print $p('World', 'How are you.');
print $p(stmt: 'How are you.', name: 'World');

Use as an identifier

Although not the primary goal, this feature would also provide a way to reference a function as a callable if needed in a given context. Specifically, omitting all parameters during application would result in a callable with the same arity as the original. For example:

function by5(int $x) { return $x * 5; }
 
$arr = [1, 2, 3, 4, 5];
 
$result = array_map(by5(?), $arr);

That would make such functions still accessible to refactoring and static analysis tools, while avoiding any new syntax.

Reflection

This RFC introduces a new reflection class, ReflectionPartial, which inherits from ReflectionFunctionAbstract. It accepts only a Partial as its single parameter, but then functions identically to any other ReflectionFunctionAbstract.

Attempting to construct a ReflectionFunction with a Partial will result in an exception, and advise the programmer to use the correct class ReflectionPartial.

A knock-on effect of that distinction is that the existing pattern of calling new ReflectionFunction(Closure::fromCallable($arbitrary_callable)) to normalize all callables to a single reflection interface will no longer work. Closure::fromCallable() will return a Partial as-is, which would then be rejected by new ReflectionFunction().

Instead, developers reflecting over callables will need to normalize their callables using a mechanism such as this:

function reflectionForCallable(callable $callable) : ReflectionFunctionAbstract {
    return $callable instanceof \Partial
        ? new ReflectionPartial($callable)
        : new ReflectionFunction(Closure::fromCallable($callable));
}

Syntax choices

The ? character was chosen for the placeholder largely because it was unambiguous and easy to implement. Prior, similar RFCs (such as the original Pipe Operator proposal from several years ago) used the $$ sigil instead. The RFC authors are open to considering other characters if they can be trivially swapped out, but would only support $$ if it gets called T_BLING.

Baring any strong consensus to the contrary, the plan is to stick with ?.

Although this RFC is stand-alone, it naturally complements a few others under current discussion.

The Pipe Operator v2 RFC proposes a new |> (pipe) operator that concatenates two callables, but was hampered by PHP's poor syntax for callables. This RFC would largely resolve that issue and allow for the following syntax for pipes:

$result = $var
|> step_one(?)
|> step_two(?, 'config')
|> $obj->stepThree('param', ?);

The original Pipes v1 proposal several years ago included similar functionality baked directly into the pipe operator. By separating the two, it allows partial application to be used generally while still offering the same convenience for the pipe use case.

Backward Incompatible Changes

None.

Implementation

Proposed PHP Version(s)

Next major/minor

rfc/partial_function_application.1620753759.txt.gz · Last modified: 2021/05/11 17:22 by crell