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

- Thread: https://externals.io/message/115166
- Original archive: https://news-web.php.net/php.internals/115166
- Started by Ralph Schindler on 2021-06-27T14:39:10+00:00
- Messages: 9
- Reply by email to internals@lists.php.net
- More for agents: search with https://externals.io/search.md?q={query} · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

---

## #115166 — Ralph Schindler — 2021-06-27T14:39:10+00:00

URL: https://externals.io/message/115166#115166

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?

---

## #115168 — Nikita Popov — 2021-06-28T09:28:03+00:00

URL: https://externals.io/message/115166#115168 · In reply to: #115166

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

---

## #115260 — Ralph Schindler — 2021-07-01T15:14:52+00:00

URL: https://externals.io/message/115166#115260 · In reply to: #115168

(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

---

## #115263 — Larry Garfield — 2021-07-01T15:31:04+00:00

URL: https://externals.io/message/115166#115263 · In reply to: #115260

> (Jump to bottom for alternative suggestion that achieves same goals)
> 
> 
> 
> > […]
> 
> Yes, only accepts named parameters (associative array),
> 
> > […]
> I would characterize this as "explicitly doesn't pass unrequested 
> parameters"
> 
> > […]
> 
> 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

---

## #115267 — Claude Pache — 2021-07-01T16:30:52+00:00

URL: https://externals.io/message/115166#115267 · In reply to: #115166

> 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

---

## #115268 — Ralph Schindler — 2021-07-01T16:55:29+00:00

URL: https://externals.io/message/115166#115268 · In reply to: #115263

> 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

---

## #115269 — Ralph Schindler — 2021-07-01T17:03:10+00:00

URL: https://externals.io/message/115166#115269 · In reply to: #115267

> 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

---

## #115271 — Hossein Baghayi — 2021-07-02T04:30:35+00:00

URL: https://externals.io/message/115166#115271 · In reply to: #115166

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,

---

## #115272 — Mike Schinkel — 2021-07-02T05:05:43+00:00

URL: https://externals.io/message/115166#115272 · In reply to: #115271

> 
> 
> Hello,
> 
> On Sun, 27 Jun 2021 at 19:09, Ralph Schindler <ralph@ralphschindler.com>
> wrote:
> 
> > […]
> 
> 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
