-
Notifications
You must be signed in to change notification settings - Fork 7.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[PoC] Associated types #18260
base: master
Are you sure you want to change the base?
[PoC] Associated types #18260
Conversation
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. |
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. |
f72c2e6
to
2baed8a
Compare
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: function f(I<T: Foo> $i) {} and this is unsound if function g(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 default
function foo(T): T;
} here as well: interface J {
type T; // invariant by default (covariant would be allowed)
function foo(): T;
} but covariant here: interface K {
type out T; // covariant
function foo(): T;
}
I confirm. There are some difficulties [3]:
[1] PHPGenerics/php-generics-rfc#45 |
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 <?php
interface I {
public function set(never $offset, never $value);
public function get(never $offset): mixed;
} With the intention that any implementation of said interface would specialize the types to be "sensible" e.g. <?php
class ListOfAnimals implements I {
public function set(int $offset, Animal $value);
public function get(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;
public function set(K $offset, V $value);
public function get(K $offset): V;
}
class ListOfAnimals implements I {
public function set(int $offset, Animal $value);
public function get(int $offset): Animal;
} This is basically also how it ties in to the Container/Offset RFC, because instead of needing to use <?php
interface DimensionReadable
{
public function offsetGet(mixed $offset): mixed;
public function offsetExists(mixed $offset): bool;
}
interface DimensionFetchable extends DimensionReadable
{
public function &offsetFetch(mixed $offset): mixed;
}
interface DimensionWritable
{
public function offsetSet(mixed $offset, mixed $value): void;
}
interface DimensionUnsetable
{
public function offsetUnset(mixed $offset): void;
}
interface Appendable
{
public function append(mixed $value): void;
}
interface FetchAppendable extends Appendable
{
public function &fetchAppend(): mixed;
} We could use a pair of associated type: <?php
interface DimensionReadable
{
type K;
type V;
public function offsetGet(K $offset): V;
public function offsetExists(K $offset): bool;
}
interface DimensionFetchable extends DimensionReadable
{
public function &offsetFetch(K $offset): V;
}
interface DimensionWritable
{
type K;
type V;
public function offsetSet(K $offset, V $value): void;
}
interface DimensionUnsetable
{
type K;
public function offsetUnset(K $offset): void;
}
interface Appendable
{
type V;
public function append(V $value): void;
}
interface FetchAppendable extends Appendable
{
public function &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. |
What I meant is that calling any method in Thank you for the explanations. |
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. function foo(SomeInterface $a) {
new GenericArray($a); // type error: type cannot be inferred from SomeConcreteType
}
foo(new SomeConcreteType()); 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. |
2baed8a
to
cc039c9
Compare
This is a proof of concept for associated types which are resolved at compile time.
Although these are not generics, they do solve the case of template types in interfaces, as generic types bound to an interface are isomorphic to associated types.
Implementation
Depends on:
The implementation currently is very crude and basic, but it demonstrates the concept of it nonetheless.
Currently, I am forcing the associated type to be invariant, as I haven't really though about how sensible it would be to have different types and how to decide which one should be the bound one.
Having a type constraint on the associated type turned out to be relatively easy as well.
This basic PoC also does not support anything relating to an interface extending another interface that has associated types, although I have an idea how it should work.
Also no opcache/JIT support.
Benefits
This would bring somewhat better semantics for the new interfaces of my Container/Offset RFC