Hey internals,
I wanted to ignite a friendly discussion whether PHP should allow typed
callable properties. The problem arises as typed properties RFC did not allow
the callable type 1 due to its context-dependent behaviour. As such, for
improving the language's consistency and adding support for strict callable
typing, I believe we should look into solutions for avoiding this special case.
Note that the solutions listed below are taken from the RFC and are thoughts
from Nikita Popov, so credits to him. Here is a quick summary of the
solutions:
- Ignore the issue. At the moment, this is the behaviour that both parameter
and return type hints pose and I believe is also the best option for callable
properties. At least for the time being. Generally, this would increase
language's consistency and would also ensure that the type is actually a
callable. In the worst-case scenario, if the callable is not callable from this
context, PHP would still throw a runtime error. - Take visibility into account. We could allow making only typed
protected/private callable properties but this would create a quite tight
coupling between the visibility and type. It would also be a special case and
therefore a violation of the OCP principle. - We could automatically wrap assignments to typed callable properties into
Closure::fromCallable(). Though this would worsen the performance and add
additional overhead when using the callable type. As Nikita mentioned, if this
is the approach we're taking, we should also impose such conversion in every
place where the callable type may occur. - Perform type checks when reading. At the moment, we perform type checks only
when writing. We could change that and also perform type checks when reading.
Although this behaviour would be weird, as we would get a type error when
trying to read a typed callable property (e. g. ($this->callback)() would
throw a type error).
Best regards,
Benas Seliuginas
P.S.: If no one is interested in implementing this, I would be up for the job
if we make a consensus on what approach we should take.
I wanted to ignite a friendly discussion whether
PHP should allow typed callable properties.
IMO no.
Trying to fix the problems with callables would be a huge amount of
work, and not actually give that much benefit. Even just documenting
the problems with callables* is a non-trivial amount of work, and I
suspect there are many horrors lurking with the SPL code related to
them.
I believe we should look into...
I'm pretty sure that choosing a different problem to solve that:
- would be easier to solve.
- provide more benefit in the long term.
- not require breaking a lot of userland + internal code immediately,
but instead allow for migration over a longer period.
My thoughts on adding 'function types' are here:
https://github.com/Danack/FunctionTypes/blob/master/function_type_rfc.md
There's still more than a couple of known problems that need to be
worked through, as well as probably unknown problems lurking. As email
is not a good format for carrying out discussions, if people want to
take part in that discussion, doing it in that repo would be better
imo.
Assuming the problems remaining can be addressed, that would give us a
more useful feature, and the existing 'callables' can be slowly
deprecated and maybe removed from PHP in the distant future.
cheers
Dan
Ack
*[problems with callables] - https://wiki.php.net/rfc/consistent_callables
Hi all,
śr., 22 kwi 2020 o 16:29 Dan Ackroyd Danack@basereality.com napisał(a):
I wanted to ignite a friendly discussion whether
PHP should allow typed callable properties.IMO no.
I agree, a callable brings as much information as a resource type - you
know the type
but are unable to use it without additional information.
Trying to fix the problems with callables would be a huge amount of
work, and not actually give that much benefit. Even just documenting
the problems with callables* is a non-trivial amount of work, and I
suspect there are many horrors lurking with the SPL code related to
them.I believe we should look into...
I'm pretty sure that choosing a different problem to solve that:
- would be easier to solve.
- provide more benefit in the long term.
- not require breaking a lot of userland + internal code immediately,
but instead allow for migration over a longer period.
I was pinged by Dan with typedef topic nearly 3 weeks ago and then started
thinking of the way to define callable types with some initial
implementation.
I chose pattern known from other languages like C# where there are types
known as delegates.
And so far got to the last line of snippet below where I have to figure out
some clever type checking with closure:
delegate Reducer (?int $sum, int $item = 0): int;
class Foo implements Reducer {
public function __invoke(?int $sum, int $item = 0): int {
return ($sum ?? 0) + $item;
}
}
function reduce(Reducer $reducer) {
var_dump($reducer(0, 5));
}
reduce(new Foo());
reduce(fn(?int $sum, int $item = 0): int => 8);
The delegate declaration resolves to an interface with __invoke method
which therefore can be easily checked
when the invokable object passed and that information can be easily cached.
Probably it can be cached also for closures
and functions but didn't get so far with the implementation yet.
I was also asked why not a general use typedef which can be used to alias
any kind of type not only a callable.
But the reason why I chose delegates was that IMO typedef is more like an
aliasing mechanism, which means
all that is possible to be aliased should also be possible to be unaliased
and pasted in all type constraints used in
function/method parameters as well as class properties.
Meaning if we allow: typedef reducer = callable(?int $sum, int $item = 0):
int;
We should also allow: function(callable(?int $sum, int $item = 0): int
$reducer) {}
Which IMO looks too verbose and that's why I think a delegate might be a
good idea as a way to provide
callable types checking.
Any thoughts are highly appreciated!
Cheers,
Michał Brzuchalski
I agree, a callable brings as much information as a resource type - you
know the type
but are unable to use it without additional information.
While this is true, it's somewhat orthogonal to the question Benas
raised in this thread - namely, how to handle values that are callable
in one scope but not another.
That is, is the following code valid (error-free) or not?
delegate Reducer (?int $sum, int $item = 0): int;
class X {
private static function myPrivateMethod(?int $sum, int $item = 0):
int {
return 0;
}
public static function runReducer(Reducer $r) {
return $r(0, 0);
}
}
echo X::bar(['X', 'myPrivateMethod']);
The equivalent code with a type constraint of "callable" currently runs
fine, even though passing the same value to a different function would
fail due to scoping. Making the delegate-constrained version throw an
error for any private or protected method would just trade one
inconsistency for another.
A more reasonable restriction, IMO, would be to say that delegates only
match Closure instances (and possibly other objects with a public
__invoke method), not the various string and array syntaxes that
"callable" currently allows. The case above would then need to bind the
private method into a closure that is callable everywhere, e.g. using
Closure::fromCallable([self::class, 'myPrivateMethod']);
The obvious downside is that Closure::fromCallable is both verbose and a
potential performance hit. So it seems we're back again to wanting a new
syntax to succinctly and optimally generate such a closure. For
instance, in the discussion of ::func, one suggestion was
"$closure={self::myPrivateMethod};" and more recently it was mentioned
that a variant of partial application is to not actually bind anything,
and write "$closure=self::myPrivateMethod(?, ?);"
Regards,
--
Rowan Tommins (né Collins)
[IMSoP]
Hello,
thank you for the comment! Yes, indeed I was raising a discussion on whether we
should allow making typed callable properties for improved language's
consistency (given that parameter/return type hints can be set to callable). As
one of the solutions, I mentioned that we could just ignore the
scope/context-dependent problems for the time being.
In regards to your comment on Closure::fromCallable()
, I do also agree that
it is quite verbose and a potential performance hit and as such, this isn't the
way to go. As an example, let's imagine a framework that allows registering
routes to callables. If we wanted to completely type-hint the entire framework,
we would have to make typed Closure properties and in the addRoute()
function
used Closure::fromCallable()
to convert every single callable into a closure
object. So, huge applications with thousands of routes would be potentially
affected by this additional overhead.
Best regards,
Benas Seliuginas
‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐
I agree, a callable brings as much information as a resource type - you
know the type
but are unable to use it without additional information.While this is true, it's somewhat orthogonal to the question Benas
raised in this thread - namely, how to handle values that are callable
in one scope but not another.That is, is the following code valid (error-free) or not?
delegate Reducer (?int $sum, int $item = 0): int;
class X {
private static function myPrivateMethod(?int $sum, int $item = 0):
int {
return 0;
}
public static function runReducer(Reducer $r) {
return $r(0, 0);
}
}
echo X::bar(['X', 'myPrivateMethod']);The equivalent code with a type constraint of "callable" currently runs
fine, even though passing the same value to a different function would
fail due to scoping. Making the delegate-constrained version throw an
error for any private or protected method would just trade one
inconsistency for another.A more reasonable restriction, IMO, would be to say that delegates only
match Closure instances (and possibly other objects with a public
__invoke method), not the various string and array syntaxes that
"callable" currently allows. The case above would then need to bind the
private method into a closure that is callable everywhere, e.g. using
Closure::fromCallable([self::class, 'myPrivateMethod']);The obvious downside is that Closure::fromCallable is both verbose and a
potential performance hit. So it seems we're back again to wanting a new
syntax to succinctly and optimally generate such a closure. For
instance, in the discussion of ::func, one suggestion was
"$closure={self::myPrivateMethod};" and more recently it was mentioned
that a variant of partial application is to not actually bind anything,
and write "$closure=self::myPrivateMethod(?, ?);"Regards,
Rowan Tommins (né Collins)
[IMSoP]
Hello,
thank you for an opinion as well! While I do fully agree that callable type
needs to be improved with features such as typedefs, I found a couple of
problems in your response.
First of all, I wouldn't say that the callable type provides little-to-no
information like the resource type. A resource can only be made by specific
functions such as fopen()
and are bound to specific use cases/implementations
e. g. MySQL resources are only for MySQL database access. Meanwhile, callables
can be made "freely" (couldn't find a better word :-)) like integers or
booleans and also in some cases don't have an exact function signature. One
such example I mention in the next paragraph.
Speaking of a snippet you showed, I'm not sure how that implementation would
work with "dynamic" callables. For example let's assume we have a framework
that allows registering routes with placeholders to controllers, like this:
$router->register('/post/{id}', [$controller, 'show']); // this would pass a single parameter to the callable
$router->register('/user/{id}/post/{id}', [$controller, 'showFromUser']); // this would pass two parameters to the callable
...in this case, we can't know how many placeholders/parameters a function can
have as that depends on how many placeholders are in the string. A framework
can only resolve these at runtime.
Best regards,
Benas Seliuginas
‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐
Hi all,
śr., 22 kwi 2020 o 16:29 Dan Ackroyd Danack@basereality.com napisał(a):
I wanted to ignite a friendly discussion whether
PHP should allow typed callable properties.IMO no.
I agree, a callable brings as much information as a resource type - you know the type
but are unable to use it without additional information.Trying to fix the problems with callables would be a huge amount of
work, and not actually give that much benefit. Even just documenting
the problems with callables* is a non-trivial amount of work, and I
suspect there are many horrors lurking with the SPL code related to
them.I believe we should look into...
I'm pretty sure that choosing a different problem to solve that:
- would be easier to solve.
- provide more benefit in the long term.
- not require breaking a lot of userland + internal code immediately,
but instead allow for migration over a longer period.I was pinged by Dan with typedef topic nearly 3 weeks ago and then started
thinking of the way to define callable types with some initial implementation.
I chose pattern known from other languages like C# where there are types
known as delegates.And so far got to the last line of snippet below where I have to figure out some clever type checking with closure:
delegate Reducer (?int $sum, int $item = 0): int;
class Foo implements Reducer {
public function __invoke(?int $sum, int $item = 0): int {
return ($sum ?? 0) + $item;
}
}
function reduce(Reducer $reducer) {
var_dump($reducer(0, 5));
}
reduce(new Foo());
reduce(fn(?int $sum, int $item = 0): int => 8);
The delegate declaration resolves to an interface with __invoke method which therefore can be easily checked
when the invokable object passed and that information can be easily cached. Probably it can be cached also for closures
and functions but didn't get so far with the implementation yet.I was also asked why not a general use typedef which can be used to alias any kind of type not only a callable.
But the reason why I chose delegates was that IMO typedef is more like an aliasing mechanism, which means
all that is possible to be aliased should also be possible to be unaliased and pasted in all type constraints used in
function/method parameters as well as class properties.Meaning if we allow: typedef reducer = callable(?int $sum, int $item = 0): int;
We should also allow: function(callable(?int $sum, int $item = 0): int $reducer) {}
Which IMO looks too verbose and that's why I think a delegate might be a good idea as a way to provide
callable types checking.Any thoughts are highly appreciated!
Cheers,
Michał Brzuchalski
Hi Benas,
my responses below.
śr., 22 kwi 2020 o 19:17 moliata moliata@protonmail.com napisał(a):
Hello,
thank you for an opinion as well! While I do fully agree that callable type
needs to be improved with features such as typedefs, I found a couple of
problems in your response.First of all, I wouldn't say that the callable type provides little-to-no
information like the resource type. A resource can only be made by specific
functions such asfopen()
and are bound to specific use
cases/implementations
e. g. MySQL resources are only for MySQL database access. Meanwhile,
callables
can be made "freely" (couldn't find a better word :-)) like integers or
booleans and also in some cases don't have an exact function signature. One
such example I mention in the next paragraph.
I only said that it brings as much value as a resource type in usage
context.
Given a resource type constraint on function argument doesn't say anything
to you
as a consumer, you cannot just use that resource without knowledge of its
type
the only thing you could do is calling a get_resource_type(resource
$resource) function.
The same applies to callable, given callable like: 'strtolower',
[DateTimeImmutable::class, 'createFromFormat']
or fn(int $x): $x*2; and when that came to your function with callable type
constraint
you also don't know how to use it without some reflection inspection cause
the type doesn't bring much value.
While fulfilling for eg. delegate gives you guarantee that when a
callable/closure is passed it matches
your expectations and you can use it right away without reflection
inspections.
You no longer need additional data to use the variable which applies to
callable and resource as well.
Speaking of a snippet you showed, I'm not sure how that implementation would
work with "dynamic" callables. For example let's assume we have a framework
that allows registering routes with placeholders to controllers, like this:
$router->register('/post/{id}', [$controller, 'show']); // this would pass a single parameter to the callable
$router->register('/user/{id}/post/{id}', [$controller, 'showFromUser']); // this would pass two parameters to the callable
...in this case, we can't know how many placeholders/parameters a function
can
have as that depends on how many placeholders are in the string. A
framework
can only resolve these at runtime.
True. In this case, it'd be hard to create a static constraint for
callable/closure check
but actually this is not the case we're looking for. Things where mentioned
delegate
match perfectly is the places where your expectations to the
callable/closure type
are static and known at the time when writing code.
Given that in a situation when the input and the output types are known
this would bring the benefit of runtime check before use.
Cheers,
Michał Brzuchalski
Hello,
thank you for expanding on your comparison of callables to resources. I didn't
completely understand what you meant but now I do.
Speaking of "dynamic" callables - this is also one of the reasons as to why I
believe we shouldn't wait for typedefs and should allow properties to be typed
as callable (just like parameters/return type). Don't get me wrong, I fully
agree that we should have more strict callables and an ability to specify their
signatures if they are static (in a sense that they don't change) and not
dynamic. But in cases like the one I mentioned, all we can do is specify that
we accept any callable.
Best regards,
Benas Seliuginas
‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐
Hi Benas,
my responses below.
śr., 22 kwi 2020 o 19:17 moliata moliata@protonmail.com napisał(a):
Hello,
thank you for an opinion as well! While I do fully agree that callable type
needs to be improved with features such as typedefs, I found a couple of
problems in your response.First of all, I wouldn't say that the callable type provides little-to-no
information like the resource type. A resource can only be made by specific
functions such asfopen()
and are bound to specific use cases/implementations
e. g. MySQL resources are only for MySQL database access. Meanwhile, callables
can be made "freely" (couldn't find a better word :-)) like integers or
booleans and also in some cases don't have an exact function signature. One
such example I mention in the next paragraph.I only said that it brings as much value as a resource type in usage context.
Given a resource type constraint on function argument doesn't say anything to you
as a consumer, you cannot just use that resource without knowledge of its type
the only thing you could do is calling a get_resource_type(resource $resource) function.The same applies to callable, given callable like: 'strtolower', [DateTimeImmutable::class, 'createFromFormat']
or fn(int $x): $x*2; and when that came to your function with callable type constraint
you also don't know how to use it without some reflection inspection cause the type doesn't bring much value.While fulfilling for eg. delegate gives you guarantee that when a callable/closure is passed it matches
your expectations and you can use it right away without reflection inspections.
You no longer need additional data to use the variable which applies to callable and resource as well.Speaking of a snippet you showed, I'm not sure how that implementation would
work with "dynamic" callables. For example let's assume we have a framework
that allows registering routes with placeholders to controllers, like this:
$router->register('/post/{id}', [$controller, 'show']); // this would pass a single parameter to the callable
$router->register('/user/{id}/post/{id}', [$controller, 'showFromUser']); // this would pass two parameters to the callable
...in this case, we can't know how many placeholders/parameters a function can
have as that depends on how many placeholders are in the string. A framework
can only resolve these at runtime.True. In this case, it'd be hard to create a static constraint for callable/closure check
but actually this is not the case we're looking for. Things where mentioned delegate
match perfectly is the places where your expectations to the callable/closure type
are static and known at the time when writing code.Given that in a situation when the input and the output types are known this would bring the benefit of runtime check before use.
Cheers,
Michał Brzuchalski
Hi Dan,
As email
is not a good format for carrying out discussions, if people want to
take part in that discussion, doing it in that repo would be better
imo.
I would be grateful if you could engage in the thread I started last
time you suggested something similar, so we can understand what people
think the problems with the current mailing list (and wiki) are, and
what tools might help solve them.
https://externals.io/message/109401
Thanks,
--
Rowan Tommins (né Collins)
[IMSoP]
Hello,
thank you for your opinion. In response to you mentioning that fixing this
problem would be a huge amount of work, I would like to reiterate the idea of
simply ignoring context-dependent problems for the time being. In fact, we can
observe this behavior in parameter/return type hints already. As such, it
doesn't make sense to make just property type hints a special case/exception.
Moreover, I oversaw Sara Golemon's comment in the mixed pseudo type
RFC that
she would like to use type aliasing and union types instead, at least in the
long run. The problem that comes up with this approach is that even if we did
something like this:
use mixed as string|int|...|callable;
...we couldn't apply this mixed
type alias to properties as callable type is
not allowed to be used with them.
Best regards,
Benas Seliuginas
‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐
I wanted to ignite a friendly discussion whether
PHP should allow typed callable properties.IMO no.
Trying to fix the problems with callables would be a huge amount of
work, and not actually give that much benefit. Even just documenting
the problems with callables* is a non-trivial amount of work, and I
suspect there are many horrors lurking with the SPL code related to
them.I believe we should look into...
I'm pretty sure that choosing a different problem to solve that:
would be easier to solve.
provide more benefit in the long term.
not require breaking a lot of userland + internal code immediately,
but instead allow for migration over a longer period.My thoughts on adding 'function types' are here:
https://github.com/Danack/FunctionTypes/blob/master/function_type_rfc.mdThere's still more than a couple of known problems that need to be
worked through, as well as probably unknown problems lurking. As email
is not a good format for carrying out discussions, if people want to
take part in that discussion, doing it in that repo would be better
imo.Assuming the problems remaining can be addressed, that would give us a
more useful feature, and the existing 'callables' can be slowly
deprecated and maybe removed from PHP in the distant future.cheers
Dan
Ack*[problems with callables] - https://wiki.php.net/rfc/consistent_callables
Hello,
I would like to correct myself (credits to Marco Pivetta for informing me)
regarding union types. While we still wouldn't be able to declare a type alias
as callable
and then use it in properties, technically speaking
string|array|object
would allow to pass callables. As such, my point about
union types is invalid.
Best regards,
Benas Seliuginas
‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐
Hello,
thank you for your opinion. In response to you mentioning that fixing this
problem would be a huge amount of work, I would like to reiterate the idea of
simply ignoring context-dependent problems for the time being. In fact, we can
observe this behavior in parameter/return type hints already. As such, it
doesn't make sense to make just property type hints a special case/exception.Moreover, I oversaw Sara Golemon's comment in the
mixed pseudo type
RFC that
she would like to use type aliasing and union types instead, at least in the
long run. The problem that comes up with this approach is that even if we did
something like this:
use mixed as string|int|...|callable;
...we couldn't apply thismixed
type alias to properties as callable type is
not allowed to be used with them.Best regards,
Benas Seliuginas‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐
I wanted to ignite a friendly discussion whether
PHP should allow typed callable properties.IMO no.
Trying to fix the problems with callables would be a huge amount of
work, and not actually give that much benefit. Even just documenting
the problems with callables* is a non-trivial amount of work, and I
suspect there are many horrors lurking with the SPL code related to
them.I believe we should look into...
I'm pretty sure that choosing a different problem to solve that:
would be easier to solve.
provide more benefit in the long term.
not require breaking a lot of userland + internal code immediately,
but instead allow for migration over a longer period.
My thoughts on adding 'function types' are here:
https://github.com/Danack/FunctionTypes/blob/master/function_type_rfc.md
There's still more than a couple of known problems that need to be
worked through, as well as probably unknown problems lurking. As email
is not a good format for carrying out discussions, if people want to
take part in that discussion, doing it in that repo would be better
imo.
Assuming the problems remaining can be addressed, that would give us a
more useful feature, and the existing 'callables' can be slowly
deprecated and maybe removed from PHP in the distant future.
cheers
Dan
Ack
*[problems with callables] - https://wiki.php.net/rfc/consistent_callables