Callable arguments cannot have default value

php.internals

Sergii Shymko

2 years ago
Hi, Wanted to bring up an inconsistent behavior of callable arguments compared to arguments of other types. Callable argument cannot have a default value (tested string or array types - both are not permitted). The same exact value works perfectly fine when passed dynamically, it just cannot be specified as a default. The workaround is to remove the type annotation which is obviously undesirable. Here’s an example: declare(strict_types=1); function test(callable $idGenerator = 'session_create_id') { $id = $idGenerator(); // ... } The function/method declaration above produces the following error on all PHP versions: Fatal error: Cannot use string as default value for parameter $idGenerator of type callable in /tmp/preview on line 4 Note that the exact same string argument can be passed without any issue: function test(callable $idGenerator) {…} test('session_create_id’); Is there a specific architectural limitation causing this that's hard/impossible to overcome? I’m aware that class properties cannot be annotated with callable - another unfortunate limitation. Callable is not a real type like other primitive types which causes all these inconsistencies, correct? Callable properties (separate topic) may be a challenge, but can at least argument defaults be supported? Regards, Sergii Shymko

Robert Landers

2 years ago
On Tue, Nov 28, 2023 at 12:59 AM Sergii Shymko <sergey@shymko.net> wrote:
> > Hi, > > Wanted to bring up an inconsistent behavior of callable arguments compared to arguments of other types. > Callable argument cannot have a default value (tested string or array types - both are not permitted). > The same exact value works perfectly fine when passed dynamically, it just cannot be specified as a default. > The workaround is to remove the type annotation which is obviously undesirable. > > Here’s an example: > declare(strict_types=1); > function test(callable $idGenerator = 'session_create_id') { > $id = $idGenerator(); > // ... > } > > The function/method declaration above produces the following error on all PHP versions: > Fatal error: Cannot use string as default value for parameter $idGenerator of type callable in /tmp/preview on line 4 > > Note that the exact same string argument can be passed without any issue: > function test(callable $idGenerator) {…} > test('session_create_id’); > > Is there a specific architectural limitation causing this that's hard/impossible to overcome? > > I’m aware that class properties cannot be annotated with callable - another unfortunate limitation. > Callable is not a real type like other primitive types which causes all these inconsistencies, correct? > Callable properties (separate topic) may be a challenge, but can at least argument defaults be supported? > > Regards, > Sergii Shymko
I stopped using "callable" a long time ago. These days I use \Closure and it works in all the same places (including properties). If you want to accept a callable string, you need to change the type to \Closure|string and verify it with `is_callable()`.
> Is there a specific architectural limitation causing this that's hard/impossible to overcome?
IIRC, default arguments must be compile-time constant, and this isn't, apparently: session_create_id(...) You can also do something like this: function hello() { echo "hi\n"; } class wrapper { public function __construct(public \Closure|string $closure) { is_callable($closure) ?: throw new InvalidArgumentException('closure must be callable'); } public function __invoke() { return ($this->closure)(); } } function test(wrapper|Closure $closure = new wrapper('hello')) { ($closure)(); } test();

Claude Pache

2 years ago
> Le 28 nov. 2023 à 00:59, Sergii Shymko <sergey@shymko.net> a écrit : > > Hi, > > Wanted to bring up an inconsistent behavior of callable arguments compared to arguments of other types. > Callable argument cannot have a default value (tested string or array types - both are not permitted). > The same exact value works perfectly fine when passed dynamically, it just cannot be specified as a default. > The workaround is to remove the type annotation which is obviously undesirable. > > Here’s an example: > declare(strict_types=1); > function test(callable $idGenerator = 'session_create_id') { > $id = $idGenerator(); > // ... > } > > The function/method declaration above produces the following error on all PHP versions: > Fatal error: Cannot use string as default value for parameter $idGenerator of type callable in /tmp/preview on line 4 > > Note that the exact same string argument can be passed without any issue: > function test(callable $idGenerator) {…} > test('session_create_id’); > > Is there a specific architectural limitation causing this that's hard/impossible to overcome? > > I’m aware that class properties cannot be annotated with callable - another unfortunate limitation. > Callable is not a real type like other primitive types which causes all these inconsistencies, correct? > Callable properties (separate topic) may be a challenge, but can at least argument defaults be supported? > > Regards, > Sergii Shymko
Hi Sergii, The big problem with the `callable` type, is that it can be check only at runtime. For instance: ```php function foo(callable $x) { } foo('strlen'); // ok foo('i_dont_exist'); // throws a TypeError ``` Another complication, is that a value of the form `[ $class, $protected_or_private_method ]` may or may not be callable depending on whether the method is visible from the current scope. In other words, contrarily to all other types, `callable` depends both on runtime state and on context. Therefore, an argument of type `callable` cannot have a default value, because it is not known in advance whether the default value will be valid when used. For the case of class properties, see https://wiki.php.net/rfc/typed_properties_v2#supported_types —Claude

Rowan Collins

2 years ago
On 28/11/2023 09:54, Claude Pache wrote:
> The big problem with the `callable` type, is that it can be check only at runtime. For instance: > > ```php > function foo(callable $x) { } > > foo('strlen'); // ok > foo('i_dont_exist'); // throws a TypeError > ```
To expand on this example, and address the original question more explicitly, consider if we allowed this: function foo(callable $x = 'maybe_exists') { } To decide whether that's a valid definition, the compiler needs to know whether 'maybe_exists' can be resolved to the name of a global function; but it might be defined in a different file, which hasn't been included yet (or, more generally, which isn't being compiled right now). To allow the default, the engine would need to defer the validity check until the function is actually executed. This is how "new in initializers" works [https://wiki.php.net/rfc/new_in_initializers] and we can actually use that feature to implement a default for callable parameters: ```php class WrappedCallable {     // Note: can't declare callable as the property type, but can as an explicit constructor parameter     private $callable;     public function __construct(callable $callable) {         $this->callable = $callable;     }     public function __invoke(...$args) { return ($this->callable)(...$args); } } function test(callable $f = new WrappedCallable('strlen')) {     echo $f('hello'); } test(); ``` Using this wrapper, we can pass in any value which is itself valid in an initializer, including callables specified as 'funcname' or ['class', 'staticmethod']. The trick is that we're not actually evaluating that value as a callable until we invoke test(), at which point the constructor of WrappedCallable performs the assertion that it's actually callable. So this compiles: function test(callable $f = new WrappedCallable('i_dont_exist')) {     echo $f('hello'); } But will then error at run-time, *unless* a global function called i_dont_exist has been defined before that call. It seems like it would be feasible for the engine to do something similar natively, creating an equivalent of WrappedCallable('i_dont_exist') using the first-class callable syntax: function test(callable $f = i_dont_exist(...)) {     echo $f('hello'); } Regards,
-- Rowan Tommins [IMSoP]