Typed callable properties

php.internals

moliata

6 years ago
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: 1. 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. 2. 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. 3. 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. 4. 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). [1]: https://wiki.php.net/rfc/typed_properties_v2#callable_type 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.

Dan Ackroyd

6 years ago
On Tue, 21 Apr 2020 at 14:08, moliata <moliata@protonmail.com> wrote:
>
> 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

Michał Brzuchalski

6 years ago
Hi all, śr., 22 kwi 2020 o 16:29 Dan Ackroyd <Danack@basereality.com> napisał(a):
> On Tue, 21 Apr 2020 at 14:08, moliata <moliata@protonmail.com> wrote: > > > > > 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

Rowan Collins

6 years ago
On 22/04/2020 16:01, Michał Brzuchalski wrote:
> 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]

moliata

6 years ago
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 ‐‐‐‐‐‐‐ On Wednesday, April 22, 2020 7:30 PM, Rowan Tommins <rowan.collins@gmail.com> wrote:

moliata

6 years ago
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 ‐‐‐‐‐‐‐ On Wednesday, April 22, 2020 6:01 PM, Michał Brzuchalski <michal.brzuchalski@gmail.com> wrote: > Hi all, > > śr., 22 kwi 2020 o 16:29 Dan Ackroyd <Danack@basereality.com> napisał(a): > >> On Tue, 21 Apr 2020 at 14:08, moliata <moliata@protonmail.com> wrote: >>> >> >>> 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

Michał Brzuchalski

6 years ago
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 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. >
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

moliata

6 years ago
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 ‐‐‐‐‐‐‐ On Wednesday, April 22, 2020 8:39 PM, Michał Brzuchalski <michal.brzuchalski@gmail.com> wrote: > 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 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. > > 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

Rowan Collins

6 years ago
Hi Dan, On 22/04/2020 15:29, Dan Ackroyd wrote:
> 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]

moliata

6 years ago
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 ‐‐‐‐‐‐‐ On Wednesday, April 22, 2020 5:29 PM, Dan Ackroyd <Danack@basereality.com> wrote:

moliata

6 years ago
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 ‐‐‐‐‐‐‐ On Wednesday, April 22, 2020 7:56 PM, moliata <moliata@protonmail.com> wrote: