Uh oh!
There was an error while loading. Please reload this page.
[PoC] Limited Abstract Generics - #18260
Conversation
withinboredom
commented
Apr 6, 2025
You are almost to full generics here :) ... Invariant is a good default (and usually the default for any generics). You only need a proper constraint resolver (this is partly why I was working on type trees in #18189, which would let you resolve covariant/contravariant constraints very easily). I'm working on that for zend_type the last couple of weeks -- which is far more complex. Potentially, between the two of us, we could enable something powerful here. I don't know if your intent is to get to full generics from here, but this is pretty similar to a couple of other experiments I've done. |
Girgias
commented
Apr 6, 2025
I was not really planning on going full generics. As the main issue with them from my understanding is determining the type to be bound to at runtime in a way that is not terrible for ergonomics and performance. Maybe @arnaud-lb could shed a bit more light. I didn't even think of a constraint resolver, but a few other people mentioned it and have an idea how to implement it, so will do that soon. |
arnaud-lb
commented
Apr 7, 2025
Interesting! In term of functionalities that has some similarities with @nikic's "purely abstract" generics [1] as well as @derickr Collections [2], in that we can not parameterize types at the point of use, but types can extend/implement parameterized types. One implication is that we can not use a type-with-assoc-types in type declarations, because this is not allowed: functionf(I<T: Foo> $i) {}and this is unsound if functiong(I$i) {}Therefore, currently this seems most useful in traits and abstract classes? Could you expand on the relation with the Container/Offset RFC? Allowing assoc types in traits or abstract classes seems possible, but this increases complexity to a level comparable to [1], as assoc types on properties, method signatures, or method bodies would be handled at runtime (at least on abstract classes). Allowing interface I {
type T; // invariant by defaultfunctionfoo(T): T;
}here as well: interface J {
type T; // invariant by default (covariant would be allowed)functionfoo(): T;
}but covariant here: interface K {
type out T; // covariantfunctionfoo(): T;
}
I confirm. There are some difficulties [3]:
[1] PHPGenerics/php-generics-rfc#45 |
Girgias
commented
Apr 7, 2025
Could you explain the unsoundness argument a bit more? I am struggling to see it. This experiment was mainly prompted about the discussion of allowing <?phpinterface I {
publicfunctionset(never$offset, never$value);
publicfunctionget(never$offset): mixed;
}With the intention that any implementation of said interface would specialize the types to be "sensible" e.g. <?phpclass ListOfAnimals implements I {
publicfunctionset(int$offset, Animal$value);
publicfunctionget(int$offset): Animal;
}The proposal to allow However, an associated type, even without being able to specify it in a type declaration, gives you at least the small guarantee that different methods use the same types: interface I {
type K : int|string
type V : mixed;
publicfunctionset(K$offset, V$value);
publicfunctionget(K$offset): V;
}
class ListOfAnimals implements I {
publicfunctionset(int$offset, Animal$value);
publicfunctionget(int$offset): Animal;
}This is basically also how it ties in to the Container/Offset RFC, because instead of needing to use <?phpinterface DimensionReadable
{
publicfunctionoffsetGet(mixed$offset): mixed;
publicfunctionoffsetExists(mixed$offset): bool;
}
interface DimensionFetchable extends DimensionReadable
{
publicfunction &offsetFetch(mixed$offset): mixed;
}
interface DimensionWritable
{
publicfunctionoffsetSet(mixed$offset, mixed$value): void;
}
interface DimensionUnsetable
{
publicfunctionoffsetUnset(mixed$offset): void;
}
interface Appendable
{
publicfunctionappend(mixed$value): void;
}
interface FetchAppendable extends Appendable
{
publicfunction &fetchAppend(): mixed;
}We could use a pair of associated type: <?phpinterface DimensionReadable
{
type K;
type V;
publicfunctionoffsetGet(K$offset): V;
publicfunctionoffsetExists(K$offset): bool;
}
interface DimensionFetchable extends DimensionReadable
{
publicfunction &offsetFetch(K$offset): V;
}
interface DimensionWritable
{
type K;
type V;
publicfunctionoffsetSet(K$offset, V$value): void;
}
interface DimensionUnsetable
{
type K;
publicfunctionoffsetUnset(K$offset): void;
}
interface Appendable
{
type V;
publicfunctionappend(V$value): void;
}
interface FetchAppendable extends Appendable
{
publicfunction &fetchAppend(): V;
}Where My main concern with supporting traits, is that I would be hitting the same issue, that I haven't resolved yet, when trying to resolve I will also say that for this feature to be fully fleshed it does need to support property hooks, which might or might not be a challenge. |
arnaud-lb
commented
Apr 7, 2025
What I meant is that calling any method in Thank you for the explanations. |
withinboredom
commented
Apr 7, 2025
I have some ideas here. Here's one I'd probably tackle first as a proof-of-concept:
If the type is affirmed, then the type in the zval can be inferred, otherwise, it is an error. functionfoo(SomeInterface$a) {
newGenericArray($a); // type error: type cannot be inferred from SomeConcreteType
}
foo(newSomeConcreteType());It's not ideal, but it is pretty straightforward to reason about as a user.
I'm working on this, but I lack a lot of practical knowledge of the engine -- but getting there, slowly but surely. Feel free to beat me to it. |
arnaud-lb
commented
Apr 21, 2025
@withinboredom this is an interesting idea as it makes inference works when the runtime and static types match. Unfortunately I think it’s unsound because calling foo() with a type accepted by its signature is an error. |
Uh oh!
There was an error while loading. Please reload this page.
14b6bb7 to
4b8cb7eCompare| --EXPECTF-- | ||
| Fatal error: Generic type cannot be part of a union type in %s on line %d |
There was a problem hiding this comment.
When I saw my example in here, I got excited that you added basic union support. Nope! 😆
One day 🙏🏻
There was a problem hiding this comment.
As I said to Bob I really want to keep it as small as possible as it's already hurting my brain a bit! But this should be a rather easy limitation to lift :)
c5cedbd to
3a8b3d2Compare3485776 to
1679e6dCompareUh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
4832ef6 to
dd46be2Compare89fe63b to
55c7f9eComparea19ba82 to
2de818eCompareOtherwise cannot left shift by 31
This is a proof of concept for a limited abstract generic types feature set, as those can be, and are, resolved at compile/linking time.
Implementation
Depends on:
Zend: Use pointer to zend_type for variance checks #18257Not needed it turns outThe implementation is relatively dumb, and partially based on arnaud-lb#4 for parser/AST/compile shenanigans.
The generic types (name and constraint) are stored on the CE in a new
generic_parameterslist field.The bound types are also stored on the CE as a HashTable:
This means that this implementation cannot be extended to support concrete generics (i.e. generics on a concrete instantiable class), as those need to be tied to the instance of the CE, not the CE itself.
The generic types must be:
It is possible to
extendan interface with generic types, so that a sub-interface can reuse the same generic parameter.If one of the generic parameters of the interface being extended has a type constraint, this type constraint must be repeated on the child interface. As the type constraints for interfaces are checked when extending.
ToDos
Benefits
Although the lack of type declarations can make this unsound, in that a generic type
Tof an interfaceI<T : C>is no better than the type constraintC(which ismixed) by default.It does "solve" the primary need of wanting
neverto be useable for parameter types, being able to specify the actual type, and thus have engine type checking, on the concrete implementation. As this is currently prevented by LSP variance rules.One use case would be to use generic types
K, Von the new interfaces of my Container/Offset RFC instead ofmixed.Future scopes
T|nullvalid)