Re: [RFC] [Discussion] Bound-Erased Generic Types
| From: | Larry Garfield | Date: | Mon, 11 May 2026 19:18:35 +0000 |
| Subject: | Re: [RFC] [Discussion] Bound-Erased Generic Types | ||
| References: | 1 2 3 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-130839@lists.php.net to get a copy of this message | ||
On Mon, May 11, 2026, at 11:25 AM, Seifeddine Gmati wrote:
> Hi Rowan,
>
> I think the core premise is empirically testable, and the test has
> already run: PHP has had generics in docblocks for a decade, used by
> every major framework. If "someone publishes a library with broken
> type information and downstream users get surprised by runtime
> behavior" were a real failure mode, we would see it today, as Laravel,
> Doctrine, Symfony, PSL, and PHPUnit all run on
>
@template-annotated
> code and are consumed by millions of downstream applications. The
> failure mode you describe is the one that would occur most under the
> current system, yet it doesn't.
>
> The reason it doesn't is that the people who care about generic type
> information are the same people who run static analysis. The
> intersection of "uses generics" and "doesn't run a static analyzer" is
> approximately empty in practice. Native syntax doesn't change that. It
> just gives current users a better way to express what they mean.
>
> Regarding the second point, the proposed implementation is enforced
> more at compile time than docblocks are. The engine validates:
>
> - Generic parameter declarations: arity cap, bound conformance,
> default-vs-bound, no top-level self-reference, no shadowing.
> - Variance soundness: Covariant parameters cannot appear in input
> positions, contravariant parameters cannot appear in output positions,
> at the declaration site.
> - Inheritance: arity at extends/implements/use, bound conformance with
> bound-on-bound for forwarded parameters, diamond detection, parametric
> LSP into properties/hooks/trait methods/inherited methods.
> - Turbofish at runtime: arity and bounds at every call site that
> supplies type arguments.
>
> Docblocks validate none of this; the engine accepts a docblock that
> says anything. Native syntax is a stricter contract than what the
> ecosystem has today.
>
> Finally, on the "broken code published to Packagist" concern: someone
> can already publish:
>
> ```
> /** Map the given array using the provided callable. */
> function map(array $array, callable $fn): array {
> return [];
> }
> ```
>
> The signature claims to take an array and a callable, returns an
> array. The body returns an empty array regardless. Today, this passes
> every PHP type check, parses without error, runs in production, and is
> technically conformant. Nobody is calling for PHP to ship a [precious
> type system](https://www.youtube.com/watch?v=Ay-gnCqDw9o) with support
> for
> [lifetimes](https://doc.rust-lang.org/rust-by-example/scope/lifetime/explicit.html),
> and [const generic
>
> parameters](https://doc.rust-lang.org/reference/items/generics.html#r-items.generics.const.usage)
> to prevent it (e.g. `function map<'a, I, O, const SIZE: int>(vec<&'a
> I, SIZE> & ![] $array, fn(&'a I): &'a O $fn): vec<&'a O,
> SIZE> &
> ![]`). The same logic applies to generics: the contract a user writes
> is the contract a user is responsible for honoring, and PHP's role is
> to provide tools for expressing and checking that contract, not to
> make every conceivable mistake impossible.
>
> The options you listed both close off paths the PHP ecosystem has
> already shown it doesn't want.
>
> Cheers,
> Seifeddine.
Hi Seifeddine.
First off, let me congratulate you on an absolutely superb "stage setting" in this RFC.
:-) The first half (the history and arguments for it) is solid, and I very much appreciate the
level of detail it includes.
That said, I tend to agree with Levi. Erased types are a foot-gun.
> The options you listed both close off paths the PHP ecosystem has
> already shown it doesn't want.
I'm not sure I follow what you're saying here, but it's worth noting that the two
points Levi mentioned have never actually been viable, so saying "the ecosystem doesn't
want it" isn't justified. I strongly suspect that the associated types work that Gina was
(and still is, AFAIK) working on would be well-received and widely used.
As you note, this RFC is a little more than erased generics; it does provide a little validation,
and reflection support (in addition to a standardized syntax that's much more understandable
than the current de facto standard docblock syntax). That's not nothing, and is a marginal
improvement over the status quo. The question is whether that is enough of a benefit, and what
future improvements it makes easier/harder.
I'm also not sure we can conclude that
> The intersection of "uses generics" and "doesn't run a static
> analyzer" is approximately empty in practice.
Technically, anyone using Symfony or Laravel is "using generics" in that Symfony and
Laravel have generic doc-types on their code. That doesn't imply that everyone building a site
with Symfony or Laravel is regularly running PHPStan to verify those, or adding their own doc-types
to match it. They *should* be, but I've worked on teams that were using Laravel and I had to
teach them about PHPStan.
Where this becomes a land-mine is less the production deploys today, but that future improvements
become BC breaks. Technically only BC breaks for sloppy code, but we've gotten ample flack in
the past for "BC breaks only for sloppy code" (eg, promoting undefined vars/keys to
warnings, adding return types to magic methods/interfaces, etc.). My fear (and I don't know
how to quantify how justified this fear is) is that people who don't use SA tools will write
code on top of someone else's generic code, not care that their types are buggy, not notice,
and then we start enforcing it in the future and their code breaks.
I wouldn't want to have a generics-version of #[ReturnTypeWillChange].
A firm and explicitly documented agreement that "if your types are wrong, that's not
covered by BC, so we don't care if it breaks on you later" would be an option; not a
popular option, perhaps, but an option. :-)
Another option, if we want to take the stance that SA is The Solution(tm) to generics, is to ship an
actual first-party SA tool with PHP. Probably not as robust or pedantic as PHPStan/Psalm/Mago, but
at the very least an enhanced alternative to the existing lint support, to catch things like type
mismatches, generics errors, etc. Yes, I realize this opens up a whole other can of worms, but
generics always opens a can of worms.
I think the associated types enforcement could absolutely be done at the same time as the
erased-for-callsite part. But if we do erased, does that make adding associated types (basically
"declaration enforcement") in the future harder? Either technically or politically? If
so, then that's a problem. If we set up the processes (both technical and political) to ensure
that it is not harder to add, then we're in a much better position.
So for me to vote in favor of this RFC, we would need to have *some* degree of first-party
enforcement, if for no other reason so that we can improve that enforcement (whether runtime or
otherwise) in the future, bit by bit. "PHP ships with what you need to write valid PHP"
is a good rule to have. We can (and assuredly will) debate what that means in practice.
As to the RFC details itself:
Syntax notes:
- I agree with Bob that the +/- syntax is very confusing. I would much rather use in/out, as seen
in Kotlin and C#, which are both vastly more self-documenting and match what some of our peer
languages are doing. As Bob notes, this can be done without completely claiming the "in"
and "out" keywords.
- An interesting quirk I found in my previous research[1] is that languages that use : for
inheritance use : for bounding generics. Languages that use "extends" for inheritance
also use "extends" for bounding generics. PHP uses "extends", so that pattern
would suggest we use "class Box<T extends FooBar>" rather than :. That's
longer, though. I'm not sure how I feel here; we certainly don't have to use the same
keyword in both places, but it could be expected. I mention it mainly so that we can have that
discussion and make a deliberate, informed decision.
Other notes:
- I am completely OK with skipping typed arrays at this point. In practice I'd rather build
objects with nice operator overrides directly into the stdlib. (See my previous investigation[2] on
the subject.)
- I fully expect "turbofish" to result in all kinds of slide shenanigans at conferences.
At least on my slides. :-)
- What would a turbofish on a static call look like? self::<Bar>foo(1) or
self::::<Bar>foo(1)? The RFC should specify.
- I'd be completely OK with lowering the argument cap from 127, too. Like, if you have more
than 4 then you're probably doing something very wrong already. :-) Reserving more space there
seems fine.
- Why is ReflectionGenericVariance backed? I don't see what the ints add.
- The "What is/isn't enforced" section could really use examples. A lot of it is
hard for me to follow as it's so abstract. (And thus determine if it's "enough"
enforcement for me to be able to support it.) Same for the Limitations section. Examples please.
- OK, having read the full RFC, it seems to be sort of "mostly-erased" generics. There is
notably more enforcement than the term "erased" would imply. (IE, I'd normally
expect that term to mean Python-style "they may as well be comments" types.) I would
recommend raising the "what is enforced" section, or some junior version of it, way up to
the top to help set expectations, because this is an interesting semi-enforced hybrid. I am still
unsure if it's enforced-enough, but I think calling it "erased" (and most people
won't automatically grok what "bound erased means") is doing it a disservice. It
looks like, in practice, basically everything on the declaration side is enforced; it's just
the call side that is unenforced. Is that an approximately accurate summary?
- I recommend making a bigger deal of the fact that the turbofish being optional is a BC layer.
Having the absence of it default to mixed is a *huge* deal for making generics adoption smoother.
This is an attribute that should get a lot more attention than the RFC currently gives it.
- If I understand correctly, this RFC would allow for foo(Collection<int> $c) {}, but
wouldn't actually enforce it at runtime. Within the function, instanceof
Collection would still work, but there's no instanceof
Collection<int> alternative. Am I reading that correctly?
- I don't see an example of how you'd write "the return value is of the class
specified by this string parameter." That's one of the status quo examples you show, and
one that I use rather frequently. Please include an example of that, or note that it is not
supported and what we should do instead.
I think at this point I am still skeptical, but warming to it, and could be convinced. But more
convincing is needed. And lunch, which I think I need after reading all of this. :-)
Thanks, Seifeddine!
--Larry Garfield
[1] https://github.com/Crell/php-rfcs/blob/master/generics/use-cases.md
[2] https://github.com/Crell/php-rfcs/blob/master/collections/research-notes.md