[Proposal] call_user_func_map(): flexible named parameter mapping for call_user_func*

php.internals

Ralph Schindler

5 years ago
The short proposal: Make a variation of this work (notice parameter order and 'b' is goes unused): call_user_func_map( fn ($c, $a) => var_dump($a, $c), [ 'a' => 'the a value', 'b' => 'the b value', 'c' => 'the c value' ] ); // string(11) "the a value" // string(11) "the c value" The long proposal: Callables, Closures, Anon Funcs, etc - have enjoyed increasing popularity year over year. We've even gotten a shorter syntax for them `fn ()` and more RFC's are sure to be on their way. This proposes a method for a publisher/framework/producer (the caller of the callback) to document all the parameters that are available as named parameters, and subscribers/consumers (creator of the callback) could subscribe to what they need by name. For example: // documented callback parameters, this is what is available: $params = [ 'a' => 'value for a', 'b' => 'value for b', 'c' => 'value for c' ]; // a consumer only interested in $a and $c could write: $f = fn ($c, $a) => var_dump($a, $c); The consumer would then register their callback with the producer/framework/etc or pass as the parameter to whatever is consuming it $callbackSystem->register('some_event', $f) // or $doSomething->with($f); The framework could then call with success: call_user_func_map($usersCallable, $allAvailableNamedParameters); Currently the only way to do this is through array_diffing parameters found via Reflection, then passing them along to call_user_func_array() as a named array. Attempting to use call_user_func_array() without specifying all provided variables results in: Uncaught Error: Unknown named parameter X in ... Thoughts?

Nikita Popov

5 years ago
On Sun, Jun 27, 2021 at 4:39 PM Ralph Schindler <ralph@ralphschindler.com> wrote:
> The short proposal: > > > Make a variation of this work (notice parameter order and 'b' is goes > unused): > > call_user_func_map( > fn ($c, $a) => var_dump($a, $c), > [ > 'a' => 'the a value', > 'b' => 'the b value', > 'c' => 'the c value' > ] > ); > > // string(11) "the a value" > // string(11) "the c value" > > > > > The long proposal: > > > Callables, Closures, Anon Funcs, etc - have enjoyed increasing > popularity year over year. We've even gotten a shorter syntax for them > `fn ()` and more RFC's are sure to be on their way. > > This proposes a method for a publisher/framework/producer (the caller of > the callback) to document all the parameters that are available as named > parameters, and subscribers/consumers (creator of the callback) could > subscribe to what they need by name. > > For example: > > // documented callback parameters, this is what is available: > $params = [ > 'a' => 'value for a', > 'b' => 'value for b', > 'c' => 'value for c' > ]; > > // a consumer only interested in $a and $c could write: > $f = fn ($c, $a) => var_dump($a, $c); > > The consumer would then register their callback with the > producer/framework/etc or pass as the parameter to whatever is consuming it > > $callbackSystem->register('some_event', $f) > > // or > > $doSomething->with($f); > > The framework could then call with success: > > call_user_func_map($usersCallable, $allAvailableNamedParameters); > > > Currently the only way to do this is through array_diffing parameters > found via Reflection, then passing them along to call_user_func_array() > as a named array. > > Attempting to use call_user_func_array() without specifying all provided > variables results in: > > Uncaught Error: Unknown named parameter X in ... > > Thoughts? >
Just to make sure I got this right: call_user_func_map() is the same as call_user_func_array(), but a) only accepts named params and b) silently ignores unknown named params? I'm not really convinced by your use-case here. This seems like a rather odd way to design an API, which goes against commonplace assumptions. If you pass a closure somewhere, the general expectation is that the parameter names you use are irrelevant and only the order matters. This creates a very unusual API contract, and while I can't prevent you from doing that, I'm pretty sure this is not something we want to endorse with additional library support. What's wrong with good old fn($params) => $params['a'] + $params['b']? (The same question for named parameters in general is answered by https://wiki.php.net/rfc/named_params#what_are_the_benefits_of_named_arguments -- notably none of the arguments made there are relevant in this context, the only benefit I see is saving a few keystrokes.) Regards, Nikita

Ralph Schindler

5 years ago
(Jump to bottom for alternative suggestion that achieves same goals)
> Just to make sure I got this right: call_user_func_map() is the same as > call_user_func_array(), but a) only accepts named params
Yes, only accepts named parameters (associative array),
> and b) silently ignores unknown named params?
I would characterize this as "explicitly doesn't pass unrequested parameters"
> I'm not really convinced by your use-case here. This seems like a rather > odd way to design an API, which goes against commonplace assumptions. If
I didn't invest a lot of thought in the use case to make the argument, ha. That said, I disagree with "odd way to design an API" sentiment... This is a common idiom in a few different places found inside frameworks. Laravel and Symfony both do named argument mapping when dispatching controller actions. Additionally, both containers do a significant amount of work with Reflection to map named arguments with potential parameters. I am not discrediting the fact that they don't also do more complex stuff, but I think this supports the argument that this method of parameters mapping does not go against commonplace assumptions. Eg: https://github.com/symfony/http-kernel/blob/5.3/HttpKernel.php#L149-L157 https://github.com/symfony/dependency-injection/search?q=reflectionMethod https://github.com/laravel/framework/blob/8.x/src/Illuminate/Container/Container.php#L649
> you pass a closure somewhere, the general expectation is that the > parameter names you use are irrelevant and only the order matters. This > creates a very unusual API contract, and while I can't prevent you from > doing that, I'm pretty sure this is not something we want to endorse > with additional library support.
> (The same question for named parameters in general is answered by > https://wiki.php.net/rfc/named_params#what_are_the_benefits_of_named_arguments > <https://wiki.php.net/rfc/named_params#what_are_the_benefits_of_named_arguments> > -- notably none of the arguments made there are relevant in this > context, the only benefit I see is saving a few keystrokes.)
Named mapping is arguably better than forced ordered mapping for all the reasons laid out in the above link. One of the primary benefits being you get type checking and better language supported analysis from tooling (like your IDE knowing the type of the parameter). This is the effective way of accomplishing what you suggested today, you can skip parameters here but get no type checking: $f = function ($params) { [$foo, $baz] = null; extract($params, EXTR_IF_EXISTS); var_dump($foo, $baz); // }; $f(['foo' => 'a', 'bar' => 'b', 'baz' => 'c']); // string(1) "a" // string(1) "c"
> What's wrong with good old fn($params) => $params['a'] + $params['b']?
...Maybe nothing, perhaps if instead of adding parameters or new functions to call_user_func_* (language construct functions) we instead can add some type checking and destructuring to it ;) # Alternative Suggestion An alternative suggestion would be to follow Javascript's lead to accomplish something similar and allow parameter destructuring, but in a more PHP way: $f = function (...[string $foo, string $baz]) { var_dump($foo, $baz); }; $f(['foo' => 'a', 'bar' => 'b', 'baz' => 'c']); // string(1) "a" // string(1) "c" -ralph

Larry Garfield

5 years ago
On Thu, Jul 1, 2021, at 10:14 AM, Ralph Schindler wrote:
> (Jump to bottom for alternative suggestion that achieves same goals) > > > > > Just to make sure I got this right: call_user_func_map() is the same as > > call_user_func_array(), but a) only accepts named params > > Yes, only accepts named parameters (associative array), > > > and b) silently ignores unknown named params? > I would characterize this as "explicitly doesn't pass unrequested > parameters" > > > I'm not really convinced by your use-case here. This seems like a rather > > odd way to design an API, which goes against commonplace assumptions. If > > I didn't invest a lot of thought in the use case to make the argument, ha. > > That said, I disagree with "odd way to design an API" sentiment... > > This is a common idiom in a few different places found inside > frameworks. Laravel and Symfony both do named argument mapping when > dispatching controller actions. Additionally, both containers do a > significant amount of work with Reflection to map named arguments with > potential parameters. > > I am not discrediting the fact that they don't also do more complex > stuff, but I think this supports the argument that this method of > parameters mapping does not go against commonplace assumptions. > > Eg: > https://github.com/symfony/http-kernel/blob/5.3/HttpKernel.php#L149-L157 > https://github.com/symfony/dependency-injection/search?q=reflectionMethod > https://github.com/laravel/framework/blob/8.x/src/Illuminate/Container/Container.php#L649
What I don't understand is what the issue with that is? You can already build up a named array and splat it into a function call to pass by name. It's a really neat trick. It doesn't need a dedicated function. About a month ago, I went through all of TYPO3 and removed all remaining call_user_func_array() calls in favor of just $function(...$args), because it does the same thing and is faster, and easier to read. Named arguments already work exactly that way, too. I don't see an advantage to adding a function that does what you can already do with less syntax: $args['foo'] = 5; $args['beep'] = get_value_from_db(); $args['narf'] = 'poink'; $callable(...$args); That works today in 8.0. We're good. --Larry Garfield

Ralph Schindler

5 years ago
> What I don't understand is what the issue with that is? > > You can already build up a named array and splat it into a function call to pass by name. It's a really neat trick. It doesn't need a dedicated function. > > About a month ago, I went through all of TYPO3 and removed all remaining call_user_func_array() calls in favor of just $function(...$args), because it does the same thing and is faster, and easier to read. Named arguments already work exactly that way, too. > > I don't see an advantage to adding a function that does what you can already do with less syntax: > > $args['foo'] = 5; > $args['beep'] = get_value_from_db(); > $args['narf'] = 'poink'; > > $callable(...$args); > > That works today in 8.0. We're good.
The proposal focuses on the onus put on the author of the closure/function, not the caller. If your $callable is only interested in $foo enforced as an int and $narf enforced as a string, what does the signature and body of the subscribing $callable look like in this case? I am proposing that if a system is providing all of this for any registered callables: $args['foo'] = 5; $args['beep'] = get_value_from_db(); $args['narf'] = 'poink'; $args['boop'] = $boopObject; $args['isNoomable'] = false; But are only interested in value for 'foo' and 'narf', they can still get type hinting/checking/enforcement and the benefits of named mapping. Let's assume the following which has a little bit of context: // developer/consumer code $library = (new SomeLibrary) $library->registerCallback(function ($foo, $narf) {}); $library->call(); // some 3rd party library with a callback system in place class SomeLibrary { public function registerCallback(callable $callback) { $this->callback = $callback; } public function call() { if (!$this->callback) { return; } $args = []; $args['foo'] = 5; $args['beep'] = 'value from db'; $args['narf'] = 'poink'; $args['boop'] = new StdClass; $args['isNoomable'] = false; ($this->callback)(...$args); } } This will fail with message: PHP Fatal error: Uncaught Error: Unknown named parameter $beep in ... Currently, the consumer/developer would have to write: $library->registerCallback(function (...$args) { $foo = $args['foo']; $narf = $args['narf']; // ... }); // or $library->registerCallback(function ($foo, $beep, $narf, $boop, $isNoomable) { // do something with foo or narf }); Additionally, the library/framework is now precluded from adding new parameters since any consuming callables would also have to introduce these new parameters as it could be considered a BC break. The more I think about it though, the more sensible this would be as it allows the callback provider to opt into named parameter mapping destructuring: $library->registerCallback(function (...[string $foo, string $narf]) { // do something with $foo and $narf }); -ralph

Claude Pache

5 years ago
> Le 27 juin 2021 à 16:39, Ralph Schindler <ralph@ralphschindler.com> a écrit : > > The short proposal: > > > Make a variation of this work (notice parameter order and 'b' is goes unused): > > call_user_func_map( > fn ($c, $a) => var_dump($a, $c), > [ > 'a' => 'the a value', > 'b' => 'the b value', > 'c' => 'the c value' > ] > ); > > // string(11) "the a value" > // string(11) "the c value" > >
Hi, If you want to ignore arguments in excess, your function may have a rest parameter collecting them. The following will run without error: ```php function foo($c, $a, ...$unused) { var_dump($a, $c); } $args = [ 'a' => 'the a value' , 'b' => 'the b value' , 'c' => 'the c value' ]; foo(...$args); ``` —Claude

Ralph Schindler

5 years ago
> If you want to ignore arguments in excess, your function may have a rest parameter collecting them. The following will run without error: > > ```php > function foo($c, $a, ...$unused) { > var_dump($a, $c); > } > > $args = [ > 'a' => 'the a value' > , 'b' => 'the b value' > , 'c' => 'the c value' > ]; > > foo(...$args); > ```
This seems like a fairly clean alternative syntax with the only downside is an unused variable. Thanks for the idea! -ralph

Hossein Baghayi

5 years ago
Hello, On Sun, 27 Jun 2021 at 19:09, Ralph Schindler <ralph@ralphschindler.com> wrote:
> > This proposes a method for a publisher/framework/producer (the caller of > the callback) to document all the parameters that are available as named > parameters, and subscribers/consumers (creator of the callback) could > subscribe to what they need by name. >
Wouldn't it rather be nice if we had more detailed callable types instead? I mean instead of simply defining a parameter type as `callable` we could specifically define what the callable expects. Something like this: ``` function bar ((int, string):bool $callback) {...} ``` A more generic approach that could be more descriptive, maybe. I have no clue if this was already discussed in other threads or not. And whether it is feasible to implement. Regards,

Mike Schinkel

5 years ago
> On Jul 2, 2021, at 12:30 AM, Hossein Baghayi <hossein.baghayi@gmail.com> wrote: > > Hello, > > On Sun, 27 Jun 2021 at 19:09, Ralph Schindler <ralph@ralphschindler.com> > wrote: > >> >> This proposes a method for a publisher/framework/producer (the caller of >> the callback) to document all the parameters that are available as named >> parameters, and subscribers/consumers (creator of the callback) could >> subscribe to what they need by name. >> > > Wouldn't it rather be nice if we had more detailed callable types instead? > I mean instead of simply defining a parameter type as `callable` we could > specifically define what the callable expects. > > Something like this: > ``` > function bar ((int, string):bool $callback) {...} > ``` > > A more generic approach that could be more descriptive, maybe. > > I have no clue if this was already discussed in other threads or not. And > whether it is feasible to implement.
From 2015 which failed (not sure why): https://wiki.php.net/rfc/callable-types But I believe that would be better to be part of a broader ability to define our own types, e.g. type function(int, string):bool IntStrFunc; function bar (intStrFunc $callback) {...} I think a few have mentioned user-defined types in the recent past but I am not aware of it being a current RFC for it. -Mike