Blog post: If we can get partial generics, should we?

php.internals

Larry Garfield

1 year ago
Hi folks. I recently wrote a blog post on behalf of the PHP Foundation about some work Gina has been doing regarding generics. It's a follow-up to last year's post on a similar topic. We are especially looking for feedback from the Internals crew, and voters in particular, on whether or not to proceed. https://thephp.foundation/blog/2025/08/05/compile-generics/ Wearing my Internals/voter hat, my stance is a very strong "yes please!"
-- Larry Garfield larry@garfieldtech.com

Vinicius Dias

1 year ago
I'm not a voter nor part of the internals, but I'd say "yes", for sure. One thing that came to mind while reading the article (keep in mind that I don't have any knowledge on the internals of PHP, so if it's a dumb question, sorry about that): Would it be possible to transform (in compile-time), the first option (that isn't supported) into the second one? ``` // Turning this: $blogPostRepository = new BaseRepository<BlogPost>(); // Into this: $blogPostRepository = new class extends BaseRepository<BlogPost> { }; ``` If this is possible, although I know it's not ideal, it would allow us to have that syntax and we wouldn't need to manually create "empty extending classes".

Thomas Nunninger

1 year ago
Hi, Am 04.08.25 um 17:23 schrieb Larry Garfield:
> Hi folks. I recently wrote a blog post on behalf of the PHP Foundation about some work Gina has been doing regarding generics. It's a follow-up to last year's post on a similar topic. We are especially looking for feedback from the Internals crew, and voters in particular, on whether or not to proceed. > > https://thephp.foundation/blog/2025/08/05/compile-generics/ > > Wearing my Internals/voter hat, my stance is a very strong "yes please!"
Thanks for that nice write-up and potentially great improvement. Can you please elaborate why it wouldn't be possible to create "empty extending classes" during compile-time of a file when there is a "`new` generic" found? Some example to make it clearer: Given a generic class ``` namespace SomeNamespace; use Some\Entity; abstract class MyGenericClass<T: Entity> { // ... } ``` and given an entity ``` namespace MyEntities; use Some\Entity; class UserEntity extends Entity { // ... } ``` when using a "`new` generic" in some file ``` namespace OtherNamespace; use SomeNamespace\MyGenericClass; use MyEntities\UserEntity; $myGenericObject = new MyGenericClass<UserEntity>; ``` and when compiling that file then the compiler creates an "empty extending class" on the fly if there is none available in the OpCache (from compiling some other file with the same "`new` generic") that has code equivalent to ``` namespace SomeNamespace; use MyEntities\UserEntity; class MyGenericClass<UserEntity> extends MyGenericClass<Entity> {} ``` That simply(TM) means, to allow `<` and `>` in class names but only internally to the engine and if generated by the compiler itself. Further question: Would it be possible to automatically mark a class as abstract if it is a generic class? Probably you have thought about this option and I just don't understand anything about internals of PHP. So don't hesitate to just answer very shortly. ;-) Best regards Thomas

Levi Morrison

1 year ago
On Mon, Aug 4, 2025 at 9:26 AM Larry Garfield <larry@garfieldtech.com> wrote:
> > Hi folks. I recently wrote a blog post on behalf of the PHP Foundation about some work Gina has been doing regarding generics. It's a follow-up to last year's post on a similar topic. We are especially looking for feedback from the Internals crew, and voters in particular, on whether or not to proceed. > > https://thephp.foundation/blog/2025/08/05/compile-generics/ > > Wearing my Internals/voter hat, my stance is a very strong "yes please!" > > -- > Larry Garfield > larry@garfieldtech.com
I played with a few iterations of Gina's work. My opinion at that time was that there are too many restrictions to be very useful. Based on those evaluations, I would not consider it to be "80% of the benefit.". With that said, maybe Gina has removed some of the restrictions. Based on the article it , but I'll commit to trying it out again. One thing the article says we're missing and that we we definitely need is unions, or at the very least the union with null. Nullable types are everywhere, and even in your interfaces it's going to show up. For instance, if we had interfaces for Set, Map, etc, there are going to be operations which return nullable values. Sure, exceptions can be thrown in some cases but not all of them. For instance, if you are building a caching data structure, throwing an exceptions on a cache miss would be very bad for performance*. You can separate it into has + get, but this has two lookups which is not desirable for performance. You can return a "tuple" like `[found, $value]` where `$value` may be null, but this loses even more type safety, and isn't a very common pattern in PHP. You really want `T|null`. There are probably other "edges" that are important and should be handled in this first pass, but missing nullable types is not a small thing. * We'll get a mini-optimization in PHP 8.5 for making the exception object faster to construct, but it doesn't speed up the backtrace, which is where most of the performance cost comes from in practice. If you are benchmarking this, make sure to include cases where your stack is a few dozen frames deep. I work for an observability company for my day job, and it is insanely normal for there to be 10+ frames, and still fairly common to be 30+ frames. Think about routing, frameworks, middleware, helper functions, libraries and abstractions, etc. If you use middleware in particular, expect 50+ frames to be normal.

Rob Landers

1 year ago
On Mon, Aug 4, 2025, at 22:49, Levi Morrison wrote:
> On Mon, Aug 4, 2025 at 9:26 AM Larry Garfield <larry@garfieldtech.com> wrote: > > > > Hi folks. I recently wrote a blog post on behalf of the PHP Foundation about some work Gina has been doing regarding generics. It's a follow-up to last year's post on a similar topic. We are especially looking for feedback from the Internals crew, and voters in particular, on whether or not to proceed. > > > > https://thephp.foundation/blog/2025/08/05/compile-generics/ > > > > Wearing my Internals/voter hat, my stance is a very strong "yes please!" > > > > -- > > Larry Garfield > > larry@garfieldtech.com > > I played with a few iterations of Gina's work. My opinion at that time > was that there are too many restrictions to be very useful. Based on > those evaluations, I would not consider it to be "80% of the > benefit.". With that said, maybe Gina has removed some of the > restrictions. Based on the article it , but I'll commit to trying it > out again. > > One thing the article says we're missing and that we we definitely > need is unions, or at the very least the union with null. Nullable > types are everywhere, and even in your interfaces it's going to show > up. For instance, if we had interfaces for Set, Map, etc, there are > going to be operations which return nullable values. Sure, exceptions > can be thrown in some cases but not all of them. For instance, if you > are building a caching data structure, throwing an exceptions on a > cache miss would be very bad for performance*. You can separate it > into has + get, but this has two lookups which is not desirable for > performance. You can return a "tuple" like `[found, $value]` where > `$value` may be null, but this loses even more type safety, and isn't > a very common pattern in PHP. You really want `T|null`. > > There are probably other "edges" that are important and should be > handled in this first pass, but missing nullable types is not a small > thing. > > * We'll get a mini-optimization in PHP 8.5 for making the exception > object faster to construct, but it doesn't speed up the backtrace, > which is where most of the performance cost comes from in practice. If > you are benchmarking this, make sure to include cases where your stack > is a few dozen frames deep. I work for an observability company for my > day job, and it is insanely normal for there to be 10+ frames, and > still fairly common to be 30+ frames. Think about routing, frameworks, > middleware, helper functions, libraries and abstractions, etc. If you > use middleware in particular, expect 50+ frames to be normal. >
I have a few different ideas for unions/intersections, but to implement them now would be refactoring for the sake of refactoring. There would be no benefit to implement any of it now. Earlier this year I tried interning types, so that type checking could be simple pointer equality. From there, I implemented a tri, such that intersections and unions were very fast to check. The result is that it was virtually useless with today’s php, except for the most pathological cases. PHP doesn’t do much type checking — currently. In any case, the changes required to pull it off (not counting opcache) were huge. If we were to run into the pathological cases more often (such as with runtime generics), it might be worth it. All that being said, we are talking about *compile-time* generics. If there is a time to be less efficient, it is during compilation, which generally happens once (esp with opcache). Further, pathological type checking is quite rare in codebases (most types are relatively simple without deep hierarchical intersections and unions), so I assume it will be fine to enable them. — Rob

Rob Landers

1 year ago
On Tue, Aug 5, 2025, at 08:19, Rob Landers wrote:
> > On Mon, Aug 4, 2025, at 22:49, Levi Morrison wrote: >> On Mon, Aug 4, 2025 at 9:26 AM Larry Garfield <larry@garfieldtech.com> wrote: >> > >> > Hi folks. I recently wrote a blog post on behalf of the PHP Foundation about some work Gina has been doing regarding generics. It's a follow-up to last year's post on a similar topic. We are especially looking for feedback from the Internals crew, and voters in particular, on whether or not to proceed. >> > >> > https://thephp.foundation/blog/2025/08/05/compile-generics/ >> > >> > Wearing my Internals/voter hat, my stance is a very strong "yes please!" >> > >> > -- >> > Larry Garfield >> > larry@garfieldtech.com >> >> I played with a few iterations of Gina's work. My opinion at that time >> was that there are too many restrictions to be very useful. Based on >> those evaluations, I would not consider it to be "80% of the >> benefit.". With that said, maybe Gina has removed some of the >> restrictions. Based on the article it , but I'll commit to trying it >> out again. >> >> One thing the article says we're missing and that we we definitely >> need is unions, or at the very least the union with null. Nullable >> types are everywhere, and even in your interfaces it's going to show >> up. For instance, if we had interfaces for Set, Map, etc, there are >> going to be operations which return nullable values. Sure, exceptions >> can be thrown in some cases but not all of them. For instance, if you >> are building a caching data structure, throwing an exceptions on a >> cache miss would be very bad for performance*. You can separate it >> into has + get, but this has two lookups which is not desirable for >> performance. You can return a "tuple" like `[found, $value]` where >> `$value` may be null, but this loses even more type safety, and isn't >> a very common pattern in PHP. You really want `T|null`. >> >> There are probably other "edges" that are important and should be >> handled in this first pass, but missing nullable types is not a small >> thing. >> >> * We'll get a mini-optimization in PHP 8.5 for making the exception >> object faster to construct, but it doesn't speed up the backtrace, >> which is where most of the performance cost comes from in practice. If >> you are benchmarking this, make sure to include cases where your stack >> is a few dozen frames deep. I work for an observability company for my >> day job, and it is insanely normal for there to be 10+ frames, and >> still fairly common to be 30+ frames. Think about routing, frameworks, >> middleware, helper functions, libraries and abstractions, etc. If you >> use middleware in particular, expect 50+ frames to be normal. >> > > I have a few different ideas for unions/intersections, but to implement them now would be refactoring for the sake of refactoring. There would be no benefit to implement any of it now. > > Earlier this year I tried interning types, so that type checking could be simple pointer equality. From there, I implemented a tri, such that intersections and unions were very fast to check. > > The result is that it was virtually useless with today’s php, except for the most pathological cases. PHP doesn’t do much type checking — currently. > > In any case, the changes required to pull it off (not counting opcache) were huge. If we were to run into the pathological cases more often (such as with runtime generics), it might be worth it. > > All that being said, we are talking about *compile-time* generics. If there is a time to be less efficient, it is during compilation, which generally happens once (esp with opcache). Further, pathological type checking is quite rare in codebases (most types are relatively simple without deep hierarchical intersections and unions), so I assume it will be fine to enable them. > > — Rob
It’s also worth pointing out that nullable or a union of null and another type is technically just one type with the nullable bit set. So, I suspect nullable types will still obey the “single type” rules proposed here. — Rob

Vinicius Dias

1 year ago
> For instance, if we had interfaces for Set, Map, etc, there are going to be operations which return nullable values.
Wouldn't it be implemented like this? ``` interface Set<T> { public function get(string $key): ?T; } ``` I might have missed something, but this doesn't seem to be a limitation of this implementation, is it?

Christian Schneider

1 year ago
Am 04.08.2025 um 17:23 schrieb Larry Garfield <larry@garfieldtech.com>:
> Hi folks. I recently wrote a blog post on behalf of the PHP Foundation about some work Gina has been doing regarding generics. It's a follow-up to last year's post on a similar topic. We are especially looking for feedback from the Internals crew, and voters in particular, on whether or not to proceed. > > https://thephp.foundation/blog/2025/08/05/compile-generics/
To be up-front: I am not the target demographic for this feature, being an OO and Type minimalist ;-) One little thing I noted in the side-note about type inference https://thephp.foundation/blog/2025/08/05/compile-generics/#type-inference was the part
> function add(int $x, int $y) > { > return $x + $y; > } > > It's readily obvious that the return type of that function is int, ...
Now this is nit-picking but that's not true for PHP in general, if I give it the values PHP_INT_MAX and 1 the result will be float, not int. It has little to do with the topic of the whole post but I still felt the urge to point it out. But please do not get into a discussion about automatic type conversions here because of that, it is just a reminder that type inference has some edge cases ;-) Regards, - Chris