Hello internals,
The discussion about allowing never types as parameter types made me think about what problem it is truly trying to solve,
which is using the same type as parameters and return values between multiple methods of a common interface.
This is normally solved with generics, but as shown generics are hard TM [1] and a lot of the complexity resides in allowing the generic type to be set at use site of the class with generic parameters.
However, this does not apply to template/generic types on interfaces, as the bound type is determined by the implementing class at compile time.
As such, a few weeks ago, I created a PoC [2] where one can declare a template/associated type T on interfaces that require an implementing class uses the same type when specifying T.
The syntax in the PoC is currently:
interface I {
type T : int|string;
public function foo(T $param): T;
}
class CS implements I {
public function foo(string $param): string {
return $param . '!';
}
}
I.e. the associated type is indicated by a "type" keyword, optionally followed by a colon :
and a type constraint.
The type corresponding to the associated type is currently "guessed" by the first usage in a concrete class.
Having talked with Arnaud off-list, it seems that using the "usual" generic syntax of (assuming our parser can cope with it):
interface I<T : int|string> {
public function foo(T $param): T;
}
class CS implements I<string> {
public function foo(string $param): string {
return $param . '!';
}
}
is possible and would not conflict with any future proposal for generics.
Importantly, this feature would NOT support doing the followings:
-
$o instanceof I<ConcreteType>
- Using
I<ConcreteType>
in type declarations
Meaning you do not get static type information, similarly to the never
as parameter type,
but one is guaranteed that there is no funky type variance and incompatibilities between different methods (or parameter and return types of the same method) in a concrete implementation of an interface.
Effectively, static analysis tools must either assume T is mixed
, or hook it into their generic types feature.
Which is what the current status is for various interfaces (e.g. ArrayAccess).
I am intending on getting this feature ready for 8.5, and the reason I bring it up to the list now, without a proper RFC, is to explain my reasoning for why I am voting against the never parameter type RFC. [3]
For any questions, feel free to reply, but please do remember that this is still a bit in flux.
Best regards,
Gina P. Banyard
[1] https://thephp.foundation/blog/2024/08/19/state-of-generics-and-collections/
[2] https://github.com/php/php-src/pull/18260
[3] https://wiki.php.net/rfc/never-parameters-v2
Hello internals,
The discussion about allowing never types as parameter types made me
think about what problem it is truly trying to solve,
which is using the same type as parameters and return values between
multiple methods of a common interface.This is normally solved with generics, but as shown generics are hard
TM [1] and a lot of the complexity resides in allowing the generic type
to be set at use site of the class with generic parameters.
However, this does not apply to template/generic types on interfaces,
as the bound type is determined by the implementing class at compile
time.As such, a few weeks ago, I created a PoC [2] where one can declare a
template/associated type T on interfaces that require an implementing
class uses the same type when specifying T.The syntax in the PoC is currently:
interface I { type T : int|string; public function foo(T $param): T; } class CS implements I { public function foo(string $param): string { return $param . '!'; } }
I.e. the associated type is indicated by a "type" keyword, optionally
followed by a colon:
and a type constraint.
The type corresponding to the associated type is currently "guessed" by
the first usage in a concrete class.Having talked with Arnaud off-list, it seems that using the "usual"
generic syntax of (assuming our parser can cope with it):interface I<T : int|string> { public function foo(T $param): T; } class CS implements I<string> { public function foo(string $param): string { return $param . '!'; } }
is possible and would not conflict with any future proposal for
generics.Importantly, this feature would NOT support doing the followings:
$o instanceof I<ConcreteType>
- Using
I<ConcreteType>
in type declarationsMeaning you do not get static type information, similarly to the
never
as parameter type,
but one is guaranteed that there is no funky type variance and
incompatibilities between different methods (or parameter and return
types of the same method) in a concrete implementation of an interface.Effectively, static analysis tools must either assume T is
mixed
, or
hook it into their generic types feature.
Which is what the current status is for various interfaces (e.g.
ArrayAccess).I am intending on getting this feature ready for 8.5, and the reason I
bring it up to the list now, without a proper RFC, is to explain my
reasoning for why I am voting against the never parameter type RFC. [3]For any questions, feel free to reply, but please do remember that this
is still a bit in flux.Best regards,
Gina P. Banyard
[1] https://thephp.foundation/blog/2024/08/19/state-of-generics-and-collections/
[2] https://github.com/php/php-src/pull/18260
[3] https://wiki.php.net/rfc/never-parameters-v2
If we make this work, I believe this solves the use cases for never
far better.
As I noted off-list (just putting it on record), if we're confident that the generic-style syntax won't interfere with future work on generics, I'd prefer that style. If it would cause issues, I'd favor the separate type
syntax or something similar, but with an explicit rather than implicit declaration of what T should be.
This would also effectively replace the custom syntax that Derick and I were considering for typed Collections a while back. A more generic solution like this would be superior either way.
--Larry Garfield
Hello internals,
The discussion about allowing never types as parameter types made me think about what problem it is truly trying to solve,
which is using the same type as parameters and return values between multiple methods of a common interface.This is normally solved with generics, but as shown generics are hard TM [1] and a lot of the complexity resides in allowing the generic type to be set at use site of the class with generic parameters.
However, this does not apply to template/generic types on interfaces, as the bound type is determined by the implementing class at compile time.As such, a few weeks ago, I created a PoC [2] where one can declare a template/associated type T on interfaces that require an implementing class uses the same type when specifying T.
The syntax in the PoC is currently:
interface I { type T : int|string; public function foo(T $param): T; } class CS implements I { public function foo(string $param): string { return $param . '!'; } }
I.e. the associated type is indicated by a "type" keyword, optionally followed by a colon
:
and a type constraint.
The type corresponding to the associated type is currently "guessed" by the first usage in a concrete class.Having talked with Arnaud off-list, it seems that using the "usual" generic syntax of (assuming our parser can cope with it):
interface I<T : int|string> { public function foo(T $param): T; } class CS implements I<string> { public function foo(string $param): string { return $param . '!'; } }
is possible and would not conflict with any future proposal for generics.
Importantly, this feature would NOT support doing the followings:
$o instanceof I<ConcreteType>
- Using
I<ConcreteType>
in type declarationsMeaning you do not get static type information, similarly to the
never
as parameter type,
but one is guaranteed that there is no funky type variance and incompatibilities between different methods (or parameter and return types of the same method) in a concrete implementation of an interface.Effectively, static analysis tools must either assume T is
mixed
, or hook it into their generic types feature.
Which is what the current status is for various interfaces (e.g. ArrayAccess).I am intending on getting this feature ready for 8.5, and the reason I bring it up to the list now, without a proper RFC, is to explain my reasoning for why I am voting against the never parameter type RFC. [3]
For any questions, feel free to reply, but please do remember that this is still a bit in flux.
Best regards,
Gina P. Banyard
[1] https://thephp.foundation/blog/2024/08/19/state-of-generics-and-collections/
[2] https://github.com/php/php-src/pull/18260
[3] https://wiki.php.net/rfc/never-parameters-v2
Hello Gina,
what I miss in this proposal is whether and how we can use the
"generic" interface as a type hint.
Importantly, this feature would NOT support doing the followings:
$o instanceof I<ConcreteType>
- Using
I<ConcreteType>
in type declarations
So, can we still do "instanceof I" or use "I" in type declarations?
And if we do, how can we then determine the associated type of an
object at runtime?
To be clear, the never type does not magically solve this problem
either, it just fills a niche.
But, there are patterns that involve the never type and union type
which can solve it..
interface AcceptsA {
public function foo(A $arg): mixed;
}
interface ReturnsR {
public function foo(never $arg): R;
}
class C implements AcceptsA, ReturrnsR {
public function foo(A $arg): R;
}
function foo(ReturnsR $obj, A $arg): R {
if ($obj instanceof AcceptsA) {
return $obj->foo($arg);
}
[...]
}
The never type also fits a natural niche in generics:
interface I {
public function foo(never $arg): void;
}
interface IT<T> extends I {
public function foo(T $arg): void;
}
We could also say that implicitly "IT<string> extends IT<never>", if T
is marked as contravariant somehow.
interface I<contravariant T1, contravariant T2> {
public function foo(T1, T2);
}
function foo(I<never, never> $obj) {
if ($obj instanceof I<string, never> && $obj instanceof I<never, int>) {...}
}
Not sure where this is going in the end, but I have the feeling that
use cases for a "never" parameter type will appear.
An interface-only generic syntax can be a step towards generics, but I
don't necessarily see it as an alternative to a "never" parameter
type.
-- Andreas
Having talked with Arnaud off-list, it seems that using the "usual" generic syntax of (assuming our parser can cope with it):
interface I<T : int|string> { public function foo(T $param): T; } class CS implements I<string> { public function foo(string $param): string { return $param . '!'; } }
is possible and would not conflict with any future proposal for generics.
I think if this works, it would be a really great step towards generics, where some use cases would be possible, and some would have easier work arounds than today. Possibly we could slowly add places the syntax is allowed, where we can make it make sense without tackling the tricky parts like variance/inheritance and type inference.
Rowan Tommins
[IMSoP]