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?
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 tocall_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
(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
(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 paramsYes, 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. IfI 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
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
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:
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
If you want to ignore arguments in excess, your function may have a rest parameter collecting them. The following will run without error:
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
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,
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 ascallable
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