[RFC] Callable Types

php.internals

Marcio Almada

10 years ago
Hello everyone, We just completed the draft for the "Callable Types" RFC. This RFC has been recently mentioned during other type related threads on this mailing list, so it feels to be the right time to put the proposal in context: The proposal is at https://wiki.php.net/rfc/callable-types The W.I.P patch is available for testing through http://3v4l.org under the RFC tab. We count with your detailed feedback and insights. Let's have a nice, respectful and constructive conversation about the RFC and work on possible improvements! Thanks Nikita Nefedov Márcio Almada

Jordi Boggiano

10 years ago
On 22/04/2016 05:12, Marcio Almada wrote:
> Hello everyone, > > We just completed the draft for the "Callable Types" RFC. This RFC has been > recently mentioned during other type related threads on this mailing list, > so it feels to be the right time to put the proposal in context: > > The proposal is at https://wiki.php.net/rfc/callable-types > > The W.I.P patch is available for testing through http://3v4l.org under the > RFC tab. > > We count with your detailed feedback and insights. Let's have a nice, > respectful and > constructive conversation about the RFC and work on possible improvements!
Very cool to see work on this. The lack of clear signature definition is definitely one of the main hurdles to using "callable" in interfaces for plugin systems and such where third parties have to integrate. Not much else to say, the syntax reads a bit weird/heavy being all in-line with the function signature but at the same time I can't imagine how else it would work that would be any better. Cheers
-- Jordi Boggiano @seldaek - http://seld.be

Levi Morrison

10 years ago
> Not much else to say, the syntax reads a bit weird/heavy being all in-line > with the function signature but at the same time I can't imagine how else it > would work that would be any better.
As mentioned on Reddit and in the future scope section of union types, we may want to introduce named type expressions: type IntReducer = callable(int, int): int; Which would also allow unions (if passed, of course): type Iterable = Array | Traversable; It would also allow for a general type-aliasing mechanism for single names. This feature has been asked for in the past but I can't remember the use-cases so this example is not very good: type Foo = SomeFoo; Then we just use the names where we would have put the expressions: function reduce(int $a, int $b, IntReducer $reducer): int { return $reducer($a, $b); }

Aaron Piotrowski

10 years ago
Hi!
> On Apr 22, 2016, at 9:39 AM, Levi Morrison <levim@php.net> wrote: > >> Not much else to say, the syntax reads a bit weird/heavy being all in-line >> with the function signature but at the same time I can't imagine how else it >> would work that would be any better. > > As mentioned on Reddit and in the future scope section of union types, > we may want to introduce named type expressions: > > type IntReducer = callable(int, int): int; > > Which would also allow unions (if passed, of course): > > type Iterable = Array | Traversable; > > It would also allow for a general type-aliasing mechanism for single > names. This feature has been asked for in the past but I can't > remember the use-cases so this example is not very good: > > type Foo = SomeFoo; > > Then we just use the names where we would have put the expressions: > > function reduce(int $a, int $b, IntReducer $reducer): int { > return $reducer($a, $b); > } > > -- > PHP Internals - PHP Runtime Development Mailing List > To unsubscribe, visit: http://www.php.net/unsub.php >
While this is somewhat off-topic of this particular RFC, since we can already do general type-aliasing with `use`, use SomeFoo as Foo; perhaps similar syntax could be used for type expressions, eliminating the need for a new keyword. use callable(int, int): int as IntReducer; use array | Traversable as Iterable; Cheers! Aaron Piotrowski @trowski2002 trowski.com

Rowan Collins

10 years ago
Aaron Piotrowski wrote on 22/04/2016 15:56:
> While this is somewhat off-topic of this particular RFC, since we can already do general type-aliasing with `use`, > > use SomeFoo as Foo; > > perhaps similar syntax could be used for type expressions, eliminating the need for a new keyword. > > use callable(int, int): int as IntReducer; > > use array | Traversable as Iterable;
The problem with that is that you might want to export (and then import) a type definition: namespace MyPackage\Types { type Foo as \Symfony\Bar | \Zend\FooBar; } namespace Other { use \MyPackage\Types\Foo as FooType; } That would become terrible confusing if you overloaded the "use" keyword. Regards,
-- Rowan Collins [IMSoP]

Rowan Collins

10 years ago
Levi Morrison wrote on 22/04/2016 15:39:
> It would also allow for a general type-aliasing mechanism for single > names. This feature has been asked for in the past but I can't > remember the use-cases so this example is not very good:
One use I've seen is for making the "semantic type" of a scalar more obvious. I'd quite like to be able to do this: type timestamp = int; function format_date(timestamp $foo) { ... } Any integer is a valid Unix timestamp, so you don't even need domain types in this case, although those would be even better: type age = int { min: 0 }; type day_of_month = int { min: 1; max: 31 }; Regards,
-- Rowan Collins [IMSoP]

Nikita

10 years ago
On Fri, 22 Apr 2016 07:12:13 +0300, Marcio Almada <marcio.web2@gmail.com> wrote:
> Hello everyone, > > We just completed the draft for the "Callable Types" RFC. This RFC has > been > recently mentioned during other type related threads on this mailing > list, > so it feels to be the right time to put the proposal in context: > > The proposal is at https://wiki.php.net/rfc/callable-types > > The W.I.P patch is available for testing through http://3v4l.org under > the > RFC tab. > > We count with your detailed feedback and insights. Let's have a nice, > respectful and > constructive conversation about the RFC and work on possible > improvements! > > Thanks > Nikita Nefedov > Márcio Almada
Hello internals, We're still having a hard time finding a proper format for error message about incompatible callable being passed. Right now it looks like this: Uncaught TypeError: Argument 3 passed to reduce() must be callable of compliant signature: callable(integer, integer): integer, callable($a, $b, $c) given, called in ... Although it goes in the fashion with all other TypeErrors but it's obviously very cryptic and it will be hard to read for human. One of the alternatives Marcio proposed was: Uncaught TypeError: Argument 3 passed to reduce() must be compliant with callable(integer, integer): integer, incompatible callable($a, $b, $c) received, called in ... This is remarkably more readable but if you have any suggestions you're welcome. Actually I think other type errors are not very readable as well, take f.e.: Argument 1 passed to foo() must be an instance of string, boolean given The fact that `string` and `boolean` are separated by only one comma, and the way `boolean given` is worded (verb after noun) makes it harder to read. So maybe we could reconsider type error messages and change them to something more user-friendly, if there's support for this idea...

Jesse Schalken

10 years ago
This is great. The gaps in PHP's type annotations are slowly being filled. :) Are the rules for compatibility between callables the same rules for compatibility between methods in classes/interfaces? Because presently, this is allowed: interface Test { public function testMethod(); } function foo1(Test $x) { } foo1(new class implements Test { public function testMethod(int $extraParam = 0) { } }); but, according to the RFC, this would not be: function foo2(callable():void $x) { } foo2(function (int $extraParam = 0) { }); I don't see why they should be different. I also didn't see anything mentioning reference parameters/returns. I assume it just follows the rules for method compatibility? On Fri, Apr 22, 2016 at 2:12 PM, Marcio Almada <marcio.web2@gmail.com> wrote:

Nikita

10 years ago
On Sat, 23 Apr 2016 10:46:38 +0300, Jesse Schalken <me@jesseschalken.com> wrote:
> This is great. The gaps in PHP's type annotations are slowly being > filled. > :) > > Are the rules for compatibility between callables the same rules for > compatibility between methods in classes/interfaces? Because presently, > this is allowed: > > interface Test { > public function testMethod(); > } > > function foo1(Test $x) { } > > foo1(new class implements Test { > public function testMethod(int $extraParam = 0) { } > }); > > > but, according to the RFC, this would not be: > > function foo2(callable():void $x) { } > > foo2(function (int $extraParam = 0) { }); > > > I don't see why they should be different. > > I also didn't see anything mentioning reference parameters/returns. I > assume it just follows the rules for method compatibility?
Hey Jesse, Thanks for reading the RFC. First and foremost - no, the rules for compatibility between callables and method implementations are different at least because our inheritance model currently doesn't support variance for every case: if you have class B extends A {} interface Foo { function foo(B $b); } then implementation of `foo()` can never have `foo(A $a)` signature, which is crucial for callables for some cases. Note that callables have runtime variance, where classes would have compile-time variance). So, in this way, callables have it right (it is desirable for classes to have variance as well, but currently it's not possible as there is a need for another compilation phase for verifying variance). What about your case of variance with extra optional parameter, it is actually not type safe with the current way of how PHP works (it's covered in the end of "Variance and Signature Validation" part). The problem lies in the notion that passing extra arguments to the function (the ones it doesn't even expect nor use) is considered valid and doesn't throw an error. So if you have a function that accepts optional argument of type B and you pass this function as a parameter of type `callable() $cb`, then it would be perfectly valid to call it as `$cb(new A);` because you assume that $cb is of type `callable()` and that means if you pass anything extra - it shouldn't Fatal. But in this case it would. While I agree that your use case is legit and ideally should be supported, I don't think it is safe to support it until PHP deprecates passing extra args to functions that don't expect them (which is very troublesome to implement due to BC). Anyway, changing this behavior of callable type later on wouldn't break BC so the gates will still be open. I've just added a part about references to the RFC (and found a bug in the implementation, thanks for that =)), I'll just copy and paste it here: Reference parameters are supported: no variance is applied to the fact whether parameter is referential or not. Example: function foo(callable(&$byref) $cb) { } foo(function (&$bar) { }); // valid foo(function ($bar) { }); // TypeError: Argument 1 passed to foo() must be callable of compliant signature: callable(&$byref), callable($bar) given function bar(callable($byval) $cb) { } bar(function (&$bar) { }); // TypeError: Argument 1 passed to bar() must be callable of compliant signature: callable($byval), callable(&$bar) given Functions returning a reference are not different from functions returning a value in PHP, for the caller, hence both are interchangeable: function foo(callable(): A $cb) { } foo(function (): A { return new A; }); foo(function &(): A { static $a; $a = $a ?: new A; return $a; }); // both would pass the boundaries of a type check (the last one has error in the current implementation, I'll fix it later)

Jesse Schalken

10 years ago
I see. So it's the combination of not erroring when more parameters are passed than a function accepts, and permitting methods to add extra optional parameters that is wrong. So without the former being fix/deprecated, the correct thing to do is to disallow extra optional params. Makes sense. Are there any valid use cases for passing extra parameters to a function any more, now that there is the varargs syntax to replace func_get_args()? If not, maybe it could be deprecated by this RFC. Regarding references, by-ref invariance for parameters makes sense, and is what method compatibility does. According to method compatibility and call-time behaviour however, return by-ref is effectively a subtype of return by-value: function &returns_ref() { $f = 0; return $f; } function returns_val() { return 1; } $v1 = returns_ref(); // ok $v2 =& returns_ref(); // ok $v3 = returns_val(); // ok $v4 =& returns_val(); // Notice: Only variables should be assigned by reference interface Methods { function &returnsRef(); function returnsVal(); } class ReturnRef implements Methods { function &returnsRef() {} // ok function &returnsVal() {} // ok } class ReturnVal implements Methods { function returnsRef() {} // Fatal error: Declaration of ReturnVal::returnsRef() must be compatible with & Methods::returnsRef() function returnsVal() {} // ok } Otherwise it LGTM cheers On Sat, Apr 23, 2016 at 8:37 PM, Nikita Nefedov <inefedor@gmail.com> wrote:

Nikita

10 years ago
On Sat, 23 Apr 2016 20:44:27 +0300, Jesse Schalken <me@jesseschalken.com> wrote:
> I see. So it's the combination of not erroring when more parameters are > passed than a function accepts, and >permitting methods to add extra > optional parameters that is wrong. So without the former being > fix/deprecated, >the correct thing to do is to disallow extra optional > params. Makes sense. > > Are there any valid use cases for passing extra parameters to a function > any more, now that there is the varargs >syntax to replace > func_get_args()? If not, maybe it could be deprecated by this RFC. > > Regarding references, by-ref invariance for parameters makes sense, and > is what method compatibility does. >According to method compatibility > and call-time behaviour however, return by-ref is effectively a subtype > of >return by-value: > >> function &returns_ref() { >> $f = 0; >> return $f; >> } >> >>>> function returns_val() { >> return 1; >> } >> >>>> $v1 = returns_ref(); // ok >> $v2 =& returns_ref(); // ok >> $v3 = returns_val(); // ok >> $v4 =& returns_val(); // Notice: Only variables should be assigned by >> reference >> >> interface Methods { >> function &returnsRef(); >> function returnsVal(); >> } >> >>>> class ReturnRef implements Methods { >> function &returnsRef() {} // ok >> function &returnsVal() {} // ok >> } >> >>>> class ReturnVal implements Methods { >> function returnsRef() {} // Fatal error: Declaration of >> ReturnVal::returnsRef() must be compatible with >>& >> Methods::returnsRef() >> function returnsVal() {} // ok >> } > > Otherwise it LGTM > > cheers
Hey, sorry for slow reply, been busy working... I don't really see use cases for func_get_args any more, variadics do it better (more obvious/documentative for the caller). But this is a huge BC break, I'm afraid it could be deprecated only in 8.0 and still that would be pretty big of an event... Definitely a matter of another RFC. You're right about return references, I've made changes in the RFC about it. There's a problem with syntax though. So now passing a function that returns a reference to a parameter that expects value returning function is working. But you can't declare in the callable typehint that you only need functions that return reference. That is, apart from syntax limitations, I'm always happy to discourage usage of references :P Touching optional parameters again - there was also my oversight, actually passing function with optional parameters is fine *as long as they are not typed*. Otherwise it can lead to weird type errors. So that was changed as well. Thanks to 3v4l guys, all these changes are on https://3v4l.org/ already, you can check them out :)

Dan Ackroyd

10 years ago
On 22 April 2016 at 05:12, Marcio Almada <marcio.web2@gmail.com> wrote:
> Hello everyone, > > We just completed the draft for the "Callable Types" RFC.
There seems to be one thing missing from the RFC; please could you add an example where the parameter and return types are also 'typed' callables? Presumably it would look something like this? function reduce(int $a, int $b, callable(int, callable(int, int):int $math):int $reducer): callable(int, int):int { return $reducer($a, $b); } Are there any limits to how far down the callable type can be defined ? cheers Dan

Nikita

10 years ago
On Sat, 23 Apr 2016 14:18:56 +0300, Dan Ackroyd <danack@basereality.com> wrote:
> On 22 April 2016 at 05:12, Marcio Almada <marcio.web2@gmail.com> wrote: >> Hello everyone, >> >> We just completed the draft for the "Callable Types" RFC. > > There seems to be one thing missing from the RFC; please could you add > an example where the parameter and return types are also 'typed' > callables? > > Presumably it would look something like this? > > function reduce(int $a, int $b, callable(int, callable(int, int):int > $math):int $reducer): callable(int, int):int { > return $reducer($a, $b); > } > > Are there any limits to how far down the callable type can be defined ? > > cheers > Dan
Hey Dan, thanks for reviewing. I have just added a paragraph about nested callables, I'll copy paste it here: Nested callables can be used with no imposed limit on the nesting level. function foo(callable(callable(int)) $cb) { $cb(function (int $i) { var_dump($i); }); } foo(function (callable(int) $intPrinter) { $intPrinter(123); }); There's currently no way to reference callable signature from within itself, meaning there's no way to make recursive signatures like below: function bar(callable(int $number): callable(int $number): callable(int $number): parent_callable $recursiveCb) { // this wouldn't work currently } To add to that, nested callables can get pretty unreadable quickly, both of these problems would be best solved by a typedef feature of some kind, added to PHP later.

Nikita

10 years ago
Bringing this thread to the main one On Sun, 24 Apr 2016 06:27:39 +0300, Mathieu Rochette <mathieu@rochette.cc> wrote:
> I've seen someone mentioning variadics so I made a few small tests, I > think those two should work (they currently don't): > > https://3v4l.org/eZgR9/rfc#rfc-callable_typehint > https://3v4l.org/N7i0u/rfc#rfc-callable_typehint > > and this one should not: > > https://3v4l.org/fcRlT/rfc#rfc-callable_typehint
Hey, You're right, this code should be valid function foo(callable() $a) { $a(); } foo(function (...$args) {}); But it gets into the same problems like optional params when it is typed function foo(callable() $a) { $a(123); } foo(function (A ...$args) {}); Now to the question why would `foo()` call `$a` with additional arguments even though it assumes `$a` is of type `callable()`? Well at first because it can - this is enough for it to be not type safe :P Long answer is: in the real life there will definitely be cases where this would allow users to shoot their feet, take this example: function map(/* array | Traversable */ $collection, callable($value, $index) $mapper) { $out = []; foreach ($collection as $index => $value) { $out[] = $mapper($value, $index); } return $out; } ... function renderPost(Post $p, bool $asAdmin = false) { return render("post_template", ["post" => $p, "asAdmin" => $asAdmin]); } ... $renderedPosts = map($posts, "renderPost"); // here developer would get fatal at the line `$out[] = $mapper($value, $index);` // if optional parameters were supported // Without them though, he gets TypeError when he passes "renderPost" to map() // And it says nicely what exactly he has done wrong Even now though, he could get into this situation if `$asAdmin` parameter was not properly typed, but that would be on him as he'd need to voluntarily drop those types for which I don't see a reason to do so. Now talking about an immediate solution to hypothetical problem we are facing there is an easy solution - if you have a functional library, you'll probably have there `partial_any` function of some kind (ex. [0]), so what you could instead do is: $renderedPosts = map($posts, f\partial_any("renderPost", f\placeholder(), true)); This would always pass `true` to a second parameter, but leave the first one. [0] https://github.com/lstrojny/functional-php/blob/master/docs/02-partial-application.md#partial_any