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 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
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);
}
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]
Hi!
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);
}--
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
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]
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...
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:
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
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)
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, 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 havefoo(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 typecallable() $cb
, then
it would be perfectly valid to call it as$cb(new A);
because you
assume that $cb is of typecallable()
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)
givenfunction bar(callable($byval) $cb) { } bar(function (&$bar) { }); // TypeError: Argument 1 passed to bar()
must be callable of compliant signature: callable($byval), callable(&$bar)
givenFunctions 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)
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
referenceinterface 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 :)
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
On Sat, 23 Apr 2016 14:18:56 +0300, Dan Ackroyd danack@basereality.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.
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_typehintand this one should not:
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