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,
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 4Note 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();
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 4Note 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:
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
The big problem with the
callable
type, is that it can be check only at runtime. For instance: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:
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]