[RFC] [Discussion] Never parameters

php.internals

Daniel Scherzer

1 year ago
Hi internals, I'd like to start discussion on a new RFC about allowing `never` for parameter types when declaring a method. * RFC: https://wiki.php.net/rfc/never-parameters-v2 * Implementation: https://github.com/php/php-src/pull/18016 -Daniel

[ ]

1 year ago
> Hi internals, > > I'd like to start discussion on a new RFC about allowing `never` for > parameter types when declaring a method. >
At first signature seemed somewhat confusing to me, but now it makes perfect sense. Type-wise, about LSP, I find it logical that more concrete implementations could define more generic parameter types without breaking the interface. In this respect any type is more generic than never. Yet, do you think it's reasonable that "never" type should be used rather than "void"? From what I know, never - is a special type used when function never returns, and always dies or throws an exception. On the other hand, void is just an indication that function never returns (but doesn't die). In my opinion, it would make more sense to use void parameters. Also, I remember that in C / C++ language there's such a concept as "void pointer", - a completely legitimate structure (likely the same concept as here) that could point to address of the data of any type. Thank you

Ilija Tovilo

1 year ago
Hi Daniel On Mon, Mar 10, 2025 at 8:06 PM Daniel Scherzer <daniel.e.scherzer@gmail.com> wrote:
> > I'd like to start discussion on a new RFC about allowing `never` for parameter types when declaring a method. > > * RFC: https://wiki.php.net/rfc/never-parameters-v2 > * Implementation: https://github.com/php/php-src/pull/18016
Thank you for your proposal! I would have slightly preferred to investigate associated types first. They mostly solve the same problem, except they can express relationships between other parameters or return types. For example: interface Enum { public type BackingType; // Potentially with `: int|float`? public function from(BackingType $value): static; public function tryFrom(BackingType $value): ?static; } This has a slightly more accurate signature, namely enforcing that from() and tryFrom() take the same argument type. Furthermore, you can use the same associated type in return types, whereas mixed would have to be used without them. I'd also argue it's easier to understand without deep knowledge of type systems. Associated types are similar to what a generic interface would give you, except: 1. Every class implementing this interface can only implement it once, restricting the associated type to one concrete type. This makes sense for PHP, given that we don't have method overloading, which would be required to make use of multiple generic interface implementations. 2. It's much simpler to implement than generics, given that this can be fully enforced during inheritance rather than runtime. I understand that associated types are still significantly more work than a never type, so I don't mind going with this solution. Given that BackedEnum cannot be implemented by users, migrating it to use associated types could be done without BC breaks. Ilija

John Bafford

1 year ago
Hi Daniel,
> On Mar 11, 2025, at 08:05, Daniel Scherzer <daniel.e.scherzer@gmail.com> wrote: > > Hi internals, > > I'd like to start discussion on a new RFC about allowing `never` for parameter types when declaring a method. > > * RFC: https://wiki.php.net/rfc/never-parameters-v2 > * Implementation: https://github.com/php/php-src/pull/18016 > > -Daniel
The RFC says that:
> never can be used as a parameter type, subject to the following restrictions > > * it cannot be used in the declaration of any method that has an implementation. Methods with never can never (pun intended) be called, because there are no values that satisfy the type constraint. Instead of delaying the error until runtime when the user attempts to call the method, just require that the method not have a body.
I would point out that `never` can be conceptually represented as a caseless enum. Right now, code like this is valid (as in, does not produce errors, though in practice, it's also not callable):
> enum Never {} > function foo(Never $never) { echo "wat"; } > function bar(Never $never) { foo($never); }
If we're going to deny the use of `never` as a parameter to a function with a body (even an empty body), perhaps we should also consider the caseless enum case. For the sake of consistency, we might want to deny use of caseless enums as function parameters. Alternatively, we might want to explicitly allow that use case to allow for a "I know I'm going to have cases here, but I don't know what they will be, but I'm still going to write some functional code around this first and would very much like my code to compile even though I haven't actually added any cases yet." scenario. Which I have definitely done before. Either way, it's an inconsistency worth clarifying. This is a bit future-gazing, but, when considered in the context of generics, it's normal and useful to have class members and bodied functions with (generically) never types. You might see this, for example, with a Result<Success, Failure> type, where Failure == Never. We would want to allow those use-cases, and disallowing it for non-generic functions would be an inconsistency. -John

Daniel Scherzer

1 year ago
[responding to multiple people instead of spamming multiple emails, I hope that is okay] ---- On Mon, Mar 10, 2025 at 12:38 PM Eugene Sidelnyk <zsidelnik@gmail.com> wrote:
> > Yet, do you think it's reasonable that "never" type should be used rather > than "void"? From what I know, never - is a special type used when function > never returns, and always dies or throws an exception. >
Never is a bottom type that means "absolutely no value is possible" - for function returns, since `return;` implicitly means `return null;` in terms of what the caller receives (https://3v4l.org/TNBFE), the only way that this type is usable is when the function never returns (dies or throws). On the other hand, void is just an indication that function never returns
> (but doesn't die). >
From my understanding, `void` is an indication that a function returns no value, but from a
> > In my opinion, it would make more sense to use void parameters. > > Also, I remember that in C / C++ language there's such a concept as "void > pointer", - a completely legitimate structure (likely the same concept as > here) that could point to address of the data of any type. >
A void pointer that can point to the address of any type is essentially the opposite of what I am trying to accomplish - coming from C/C++, I would assume a `void` parameter would therefore be the *top* type, allowing anything, while `never` is meant to be the *bottom* type, allowing nothing. ---- On Mon, Mar 10, 2025 at 2:44 PM Ilija Tovilo <tovilo.ilija@gmail.com> wrote: I would have slightly preferred to investigate associated types first.
> They mostly solve the same problem, except they can express > relationships between other parameters or return types. For example:
I had never heard of associated types before this email thread - google suggests that they are a feature of rust and swift, neither of which I have used. While it certainly sounds interesting, it looks like a lot more work, and since if we introduce associated types later we can change BackedEnum without a BC break, I don't think a future possibility of associated types should stand in the way of `never` types now. ---- On Mon, Mar 10, 2025 at 3:11 PM John Bafford <jbafford@zort.net> wrote:
> > I would point out that `never` can be conceptually represented as a > caseless enum. Right now, code like this is valid (as in, does not produce > errors, though in practice, it's also not callable): >
While a caseless enum might represent the concept of a parameter than can never be valid, subclasses cannot just widen a caseless-enum-never to just (e.g.) an `int`, it would need to be `Never|int`. I'll leave further discussion of caseless enums for another thread since they are not equivalent from a type-theory perspective. ---- On Mon, Mar 10, 2025 at 3:59 PM Rob Landers <rob@bottled.codes> wrote: This looks interesting. I'm not sure that I like "never" as a parameter
> type and while it "technically" doesn't violate LSP, it seems like a > backdoor to doing just that:
So the initial inspiration for this was the BackedEnum class, where it isn't a technicality - LSP is violated when the interface allows both strings and ints, but no actual enum can use both. I think odd code like subclasses of `Point` not accepting reasonable things is something that should be caught by other developers doing code review, not enforced on a language level. For example (https://3v4l.org/pTdMg) ``` abstract class Point { abstract public function add(Point $other); abstract public function subtract(Point $other); } class Vector2 extends Point { public function add(Point|Banana $other) { if (!$other instanceof Banana) { throw new TypeError("Point only allowed to prevent compiler errors"); } // ...rest of the function } public function subtract(Point|Football $other) { if (!$other instanceof Football) { throw new TypeError("Point only allowed to prevent compiler errors"); } // ...rest of the function } } ``` There's basically only a gentleman's agreement that a subclass will
> implement things in a way that makes sense.
I think that this agreement is present for any method that isn't final, and that doesn't change now - the only difference is that now the PHP compiler won't get in the way I would also personally prefer associated types: ... This at least lets you
> ensure the "other point" is the same type in both functions, though > personally, I'd rather just have generics.
I'd also like to have generics, but that isn't something I can implement myself. Associated types would be interesting, but I don't think that associated types would remove the entire use-case for `never` parameters, just perhaps the specific example of BackedEnum. ---- - Daniel

Rob Landers

1 year ago
> >> I would also personally prefer associated types: ... This at least lets you ensure the "other point" is the same type in both functions, though personally, I'd rather just have generics. > > I'd also like to have generics, but that isn't something I can implement myself. Associated types would be interesting, but I don't think that associated types would remove the entire use-case for `never` parameters, just perhaps the specific example of BackedEnum. > > ---- > - Daniel
Heh, this is the long game I am playing with inner classes: https://externals.io/message/125049#125057 If inner classes can be implemented, then we are just a short hop from being able to implement generics with a similar approach to what I outlined in that thread. Granted, I'm making some major changes to the RFC at the moment and the implementation -- based on feedback, so it'll be a few days before that is finished. An inner class view of generics (as opposed to the type aliasing view in that thread) looks something like this: // Box<T> class Box { public class T {} // store(T $item) public function store(static:>T $item) {} } // new Box<ItemType>() $box = new class() extends Box { public class ItemType as T {} }; -Ish. Note that the example won’t actually work; but it illustrates how the engine could implement it if there were inner/nested types. So, assuming this new implementation I’ve been working on is waaay better than the old one (I think it is), and the RFC is accepted; we may be closer to generics than you thought. Or maybe there is someone out there with a whole different way of doing it that is even better… This is the approach I'm chasing though. — Rob

Rob Landers

1 year ago
On Mon, Mar 10, 2025, at 20:05, Daniel Scherzer wrote:
> Hi internals, > > I'd like to start discussion on a new RFC about allowing `never` for parameter types when declaring a method. > > * RFC: https://wiki.php.net/rfc/never-parameters-v2 > * Implementation: https://github.com/php/php-src/pull/18016 > > -Daniel
Hey Daniel, This looks interesting. I'm not sure that I like "never" as a parameter type and while it "technically" doesn't violate LSP, it seems like a backdoor to doing just that: abstract class Point { function add(never $other); function subtract(never $other); } class Vector2 extends Point { public function add(Banana $other) {} public function subtract(Football $other) {} } There's basically only a gentleman's agreement that a subclass will implement things in a way that makes sense. I would also personally prefer associated types: abstract class Point { public type OtherPoint; public function add(OtherPoint $other); pubic function subtract(OtherPoint $other); } This at least lets you ensure the "other point" is the same type in both functions, though personally, I'd rather just have generics. — Rob

Larry Garfield

1 year ago
On Mon, Mar 10, 2025, at 2:05 PM, Daniel Scherzer wrote:
> Hi internals, > > I'd like to start discussion on a new RFC about allowing `never` for > parameter types when declaring a method. > > * RFC: https://wiki.php.net/rfc/never-parameters-v2 > * Implementation: https://github.com/php/php-src/pull/18016 > > -Daniel
I have a use case for this in Serde, so would be in favor. We should not block this kind of improvement on the hope of generics. Worst case, we have this plus generics so you have options, how terrible. Rust-style associated types would probably work as well. I'd be fine with that approach, too. One could argue they're more valuable as a sort of "junior generics," but absent anyone able and willing to implement them, again, worst-case we end up with options in the future. --Larry Garfield

Tim Düsterhus

1 year ago
Hi Am 2025-03-11 22:45, schrieb Larry Garfield:
> We should not block this kind of improvement on the hope of generics. > Worst case, we have this plus generics so you have options, how > terrible.
In this case, I agree. This is an obvious addition to the type system that uses the existing infrastructure of the type system. But generally speaking, having too many options is not a good thing. It makes the language larger and more complex to learn, requires documentation effort, support by IDEs and static analyzers and similar things.
> Rust-style associated types would probably work as well. I'd be fine > with that approach, too. One could argue they're more valuable as a > sort of "junior generics," but absent anyone able and willing to > implement them, again, worst-case we end up with options in the future.
… so for associated types, I do not necessarily agree. They seem to be a strict subset of the functionality enabled by generics, or the functionality enabled by type aliases combined with “inner classes / inner types”. Since introducing associated types would bring entirely new syntax and semantics to the language, it is less obvious to me that they are a useful (intermediate) addition to the language. Best regards Tim Düsterhus

Larry Garfield

1 year ago
On Thu, Mar 13, 2025, at 10:35 AM, Tim Düsterhus wrote:
> Hi > > Am 2025-03-11 22:45, schrieb Larry Garfield: >> We should not block this kind of improvement on the hope of generics. >> Worst case, we have this plus generics so you have options, how >> terrible. > > In this case, I agree. This is an obvious addition to the type system > that uses the existing infrastructure of the type system. > > But generally speaking, having too many options is not a good thing. It > makes the language larger and more complex to learn, requires > documentation effort, support by IDEs and static analyzers and similar > things.
It's definitely a balancing act, yes. TIMTOWTDI definitely has downsides, but at the same time, having multiple ways to accomplish the same goal isn't inherently wrong, unless they conflict with each other in some way.
>> Rust-style associated types would probably work as well. I'd be fine >> with that approach, too. One could argue they're more valuable as a >> sort of "junior generics," but absent anyone able and willing to >> implement them, again, worst-case we end up with options in the future. > > … so for associated types, I do not necessarily agree. They seem to be a > strict subset of the functionality enabled by generics, or the > functionality enabled by type aliases combined with “inner classes / > inner types”. Since introducing associated types would bring entirely > new syntax and semantics to the language, it is less obvious to me that > they are a useful (intermediate) addition to the language.
The counter argument here is that a number of languages (like Rust) have both generics and associated types, so they can coexist and serve slightly different use cases. I have not used Rust enough to have an opinion on how and when to use one or the other, but having one doesn't seem to impede the other. From my (admittedly limited) understanding, associated types sounds rather like the "inheritance only" pseudo-generics that Derick had proposed for collections last year, in a more general form. (cf: https://thephp.foundation/blog/2024/08/19/state-of-generics-and-collections/#collections) --Larry Garfield

Gina P. Banyard

1 year ago
On Tuesday, 11 March 2025 at 21:45, Larry Garfield <larry@garfieldtech.com> wrote:
> On Mon, Mar 10, 2025, at 2:05 PM, Daniel Scherzer wrote: > > > Hi internals, > > > > I'd like to start discussion on a new RFC about allowing `never` for > > parameter types when declaring a method. > > > > * RFC: https://wiki.php.net/rfc/never-parameters-v2 > > * Implementation: https://github.com/php/php-src/pull/18016 > > > > -Daniel > > > I have a use case for this in Serde, so would be in favor. > > We should not block this kind of improvement on the hope of generics. Worst case, we have this plus generics so you have options, how terrible. > > Rust-style associated types would probably work as well. I'd be fine with that approach, too. One could argue they're more valuable as a sort of "junior generics," but absent anyone able and willing to implement them, again, worst-case we end up with options in the future. > > --Larry Garfield
As the person that had the initial discussion in R11 with Jordan [1] never as a parameter type for an interface actually is not the solution for "poor man generics". Matthew Fonda [2] already replied to the thread pointing out the remark Nikita made in the discussion of the previous RFC. But importantly, going from mixed parameter type to a generic parameter type is *allowed* and not a BC change, however, going from a never parameter type to a generic parameter type is a BC break. Thus, I am not sure this really a good idea. The argument from Alwin is more compelling but considering we don't have conditional types, not sure if this makes sense either. Best regards, Gina P. Banyard [1] https://chat.stackoverflow.com/transcript/11?m=52810456#52810456 [2] https://externals.io/message/126698#126791

Larry Garfield

1 year ago
On Thu, Mar 20, 2025, at 11:24 AM, Gina P. Banyard wrote:
> As the person that had the initial discussion in R11 with Jordan [1] > never as a parameter type for an interface actually is not the solution > for "poor man generics". > Matthew Fonda [2] already replied to the thread pointing out the remark > Nikita made in the discussion of the previous RFC. > But importantly, going from mixed parameter type to a generic parameter > type is *allowed* and not a BC change, > however, going from a never parameter type to a generic parameter type > is a BC break.
To clarify, you're saying this: interface I { pubic function foo(mixed $a); } class C implements I { public function foo(mixed $b) { ... } } Can turn into this: interface I<A> { pubic function foo(A $a); } class C implements I<Foo> { public function foo(Foo $b) { ... } } But this could not turn into that: interface I { pubic function foo(never $a); } class C implements I { public function foo(Foo $b) { ... } } Am I following that? Because just from writing that I am not sure I agree, which means I may be misunderstanding. :-) --Larry Garfield

Gina P. Banyard

1 year ago
On Thursday, 20 March 2025 at 16:57, Larry Garfield <larry@garfieldtech.com> wrote:
> On Thu, Mar 20, 2025, at 11:24 AM, Gina P. Banyard wrote: > > > As the person that had the initial discussion in R11 with Jordan [1] > > never as a parameter type for an interface actually is not the solution > > for "poor man generics". > > Matthew Fonda [2] already replied to the thread pointing out the remark > > Nikita made in the discussion of the previous RFC. > > But importantly, going from mixed parameter type to a generic parameter > > type is allowed and not a BC change, > > however, going from a never parameter type to a generic parameter type > > is a BC break. > > > To clarify, you're saying this: > > [...] > > Am I following that? Because just from writing that I am not sure I agree, which means I may be misunderstanding. :-)
I am saying: interface I { pubic function foo(never $a); } can ***not*** be "upgraded" to interface I<A> { pubic function foo(A $a); } whereas it is possible to go from interface I { pubic function foo(mixed $a); } to interface I<A> { pubic function foo(A $a); } The implementing classes are completely irrelevant in this context. Best regards, Gina P. Banyard

Tim Düsterhus

1 year ago
Hi Am 2025-03-21 21:41, schrieb Gina P. Banyard:
>> Am I following that? Because just from writing that I am not sure I >> agree, which means I may be misunderstanding. :-) > > I am saying: > interface I { > pubic function foo(never $a); > } > > can ***not*** be "upgraded" to > > interface I<A> { > pubic function foo(A $a); > } > > whereas it is possible to go from > > interface I { > pubic function foo(mixed $a); > } > > to > > interface I<A> { > pubic function foo(A $a); > } > > The implementing classes are completely irrelevant in this context. >
I don't follow here. Neither interface “could be upgraded” to make use of generics, since the user would need to specify the type for `A`. However the former could just be upgraded to `I<never>` and the implementing class could still override the parameter type with some specific type. This would not be better than the old interface with the hardcoded `never` type, but also not worse. The latter would need to be upgraded to `I<mixed>`, since otherwise you would be restricting passing types that you formerly didn't, which makes the entire exercise useless. Best regards Tim Düsterhus

Daniel Scherzer

1 year ago
On Thu, Mar 20, 2025 at 4:00 PM Larry Garfield <larry@garfieldtech.com> wrote:
> I have a use case for this in Serde, so would be in favor. > > We should not block this kind of improvement on the hope of generics. > Worst case, we have this plus generics so you have options, how terrible. > >
Would you mind sharing details of your Serde use case? It seems that the BackedEnum example might not have been the best (since it is for static methods) and so perhaps a userland case where this would be used would help. --Daniel

Larry Garfield

1 year ago
On Thu, Mar 20, 2025, at 6:02 PM, Daniel Scherzer wrote:
> On Thu, Mar 20, 2025 at 4:00 PM Larry Garfield <larry@garfieldtech.com> wrote: >> I have a use case for this in Serde, so would be in favor. >> >> We should not block this kind of improvement on the hope of generics. Worst case, we have this plus generics so you have options, how terrible. >> > > Would you mind sharing details of your Serde use case? It seems that > the BackedEnum example might not have been the best (since it is for > static methods) and so perhaps a userland case where this would be used > would help. > > --Daniel
Simplified example to show the thing we care about: I have an interface Formatter, like: interface Formatter { public function serializeInitialize(ClassSettings $classDef, Field $rootField): mixed; public function serializeInt(mixed $runningValue, Field $field, ?int $next): mixed; public function serializeFloat(mixed $runningValue, Field $field, ?float $next): mixed; // And other methods for other types } The $runningValue is of a type known concretely to a given implementation, but not at the interface level. It's returned from serializeIntialize(), and then passed along to every method, recursively, as it writes out an object. So for instance, the JSON formatter looks like this: class JsonSerializer implements Formatter { public function serializeInitialize(ClassSettings $classDef, Field $rootField): array { return ['root' => []]; } /** * @param array<string, mixed> $runningValue * @return array<string, mixed> */ public function serializeInt(mixed $runningValue, Field $field, ?int $next): array { $runningValue[$field->serializedName] = $next; return $runningValue; } } Because JsonFormatter works by building up an array and passing it to json_encode(), eventually. So $runningValue is guaranteed to always be an array. I can narrow the return value to an array, but not the parameter. The JsonStreamFormatter, however, has a stream object that it passes around (which wraps a file handle internally): class JsonStreamFormatter implements Formatter { public function serializeInitialize(ClassSettings $classDef, Field $rootField): FormatterStream { return FormatterStream::new(fopen('php://temp/', 'wb')); } /** * @param FormatterStream $runningValue */ public function serializeInt(mixed $runningValue, Field $field, ?int $next): FormatterStream { $runningValue->write((string)$next); return $runningValue; } } Again, I can narrow the return value but not the param. To be clear, generics would absolutely be better in this case. I'm just not holding my breath. Associated Types would probably work in this case, too, since it's always relevant when creating a new concrete object, not when parameterizing a common object. If we got that, I'd probably use that instead. Changing the interface to use `never` instead of `mixed` would have the weakest guarantees of the three, since it doesn't force me to use the *same* widened type on serializeInt(), serializeFloat(), serializeString(), etc., even though it would always be the same. But it would allow me to communicate more type information than I can now. How compelling this use case is, I leave as an exercise for the reader. --Larry Garfield

Alwin Garside

1 year ago
On 10 Mar 2025, at 20:07, Daniel Scherzer <daniel.e.scherzer@gmail.com> wrote:
> Hi internals, > > I'd like to start discussion on a new RFC about allowing `never` for parameter types when declaring a method. > > * RFC: https://wiki.php.net/rfc/never-parameters-v2 > * Implementation: https://github.com/php/php-src/pull/18016 > > -Daniel
Hi Daniel, To begin, I'm all for a `never` bottom type, and I use it regularly when I need to write TypeScript. However, I think the example offered in this RFC – while a valid one, for now – isn't the most appropriate use case for a `never` type. A bottom type is more often used when defining conditional types, or to assert that the default for a switch/match expression should never be reached without raising an error that the switch/match cases are not exhaustive. As such, a `never` type provides a lot of value for static analysis tools and IDEs, and as a matter of fact both PHPStan[1] and PhpStorm[2] provide ways to declare a bottom type. Furthermore, when conditional return types and/or generics are finally implemented in PHP, never will become even more valuable if not essential. But, when generics eventually do make their way to PHP, it also means the provided example no longer applies, because you would just be able to use generic types: ```php interface BackedEnum<T> extends UnitEnum { public T $value; public static function from(T $value): static; public static function tryFrom(T $value): ?static; } ``` So I think it may help sell your RFC if you provide some additional context illustrating the use of the `never` type in generics and static analysis. Otherwise I fear that some people may interpret `never` as a stopgap solution until we get generics. Alwin [1] https://phpstan.org/writing-php-code/phpdoc-types#bottom-type [2] https://github.com/JetBrains/phpstorm-attributes/blob/master/README.md#noreturn

Matthew Fonda

1 year ago
On Mon, Mar 10, 2025 at 12:07 PM Daniel Scherzer < daniel.e.scherzer@gmail.com> wrote:
> Hi internals, > > I'd like to start discussion on a new RFC about allowing `never` for > parameter types when declaring a method. > > * RFC: https://wiki.php.net/rfc/never-parameters-v2 > * Implementation: https://github.com/php/php-src/pull/18016 > > -Daniel >
Hi Daniel, I believe this feature essentially amounts to "add methods which can never be called", which in my mind makes no sense. If a method types against an interface, and that interface uses a method with a never parameter type, then we cannot actually call that method. We'd need to know the specific concrete type, which defeats the purpose of using an interface in the first place. See note from Nikita [1] from previous discussion which expands on this idea more, and shows that generics is really what we need here. [1] https://externals.io/message/115712#115719 Best regards, --Matthew

Daniel Scherzer

1 year ago
On Sun, Mar 16, 2025 at 12:31 PM Matt Fonda <matthewfonda@gmail.com> wrote:
> Hi Daniel, > > I believe this feature essentially amounts to "add methods which can never > be called", which in my mind makes no sense. If a method types against an > interface, and that interface uses a method with a never parameter type, > then we cannot actually call that method. We'd need to know the specific > concrete type, which defeats the purpose of using an interface in the first > place. >
First, I would point out that I tried to make this limited to methods that already cannot be called - only to interfaces and abstract methods. You already can't call those directly, you need to have a subclass. And you can always type against the implementation with the actual methods that are not never-typed. So I would ask - if not `never` parameters, then what should users do? It seems like, for the use case of "the interface adds a method but there are no promises about what parameters it accepts", the options would otherwise be * documenting that the method should exist, and checking for it, but not adding it to the interface directly so that PHP doesn't complain * having implementations "accept" parameters that they then manually throw errors for, c.f. https://3v4l.org/pTdMg The original inspiration which I discussed in the RFC is fixing the signature of the BackedEnum methods, which currently use the second option.
> > See note from Nikita [1] from previous discussion which expands on this > idea more, and shows that generics is really what we need here. > > [1] https://externals.io/message/115712#115719 > > Best regards, > --Matthew >
I agree that generics would be great and really useful, but should that stop never parameters? There has also been some discussion about generics on the inner classes RFC thread - should the dream of generics eventually stand in the way of independently-useful and smaller-scoped features now? -Daniel

Juris Evertovskis

1 year ago
On 2025-03-20 18:50, Daniel Scherzer wrote:
> The original inspiration which I discussed in the RFC is fixing the > signature of the BackedEnum methods, which currently use the second > option. > >>
Hey, I'm not opposed to having a bottom type, but the `try`/`tryFrom` issue feels caused by another shortcoming. In my view`from` and `tryFrom` are factory methods that construct an instance by being called on the class itself. As such they should not be subject to LSP at all, just like __construct is not. BR, Juris

Tim Düsterhus

1 year ago
Hi Am 2025-03-20 21:04, schrieb Juris Evertovskis:
> I'm not opposed to having a bottom type, but the `try`/`tryFrom` issue > feels caused by another shortcoming. > > In my view`from` and `tryFrom` are factory methods that construct an > instance by being called on the class itself. As such they should not > be subject to LSP at all, just like __construct is not.
Indeed. I said the same in the PR on GitHub: https://github.com/php/php-src/pull/18016#issuecomment-2715887293. Static methods within an interface are not useful. The `never` type would however still be useful for non-static methods in interfaces and to round off the type hierarchy. Best regards Tim Düsterhus

Matthew Fonda

1 year ago
On Thu, Mar 20, 2025 at 9:51 AM Daniel Scherzer <daniel.e.scherzer@gmail.com> wrote:
> On Sun, Mar 16, 2025 at 12:31 PM Matt Fonda <matthewfonda@gmail.com> > wrote: > >> Hi Daniel, >> >> I believe this feature essentially amounts to "add methods which can >> never be called", which in my mind makes no sense. If a method types >> against an interface, and that interface uses a method with a never >> parameter type, then we cannot actually call that method. We'd need to know >> the specific concrete type, which defeats the purpose of using an interface >> in the first place. >> > > First, I would point out that I tried to make this limited to methods that > already cannot be called - only to interfaces and abstract methods. You > already can't call those directly, you need to have a subclass. And you can > always type against the implementation with the actual methods that are not > never-typed. >
To clarify, what I mean by calling methods on an interface is typing against the interface (for example, as a parameter to a method/function) and then calling methods defined by the interface. As users of the interface, we can call methods on it without knowing or caring which specific subclass we're actually working with. In my mind, this is the entire point of interfaces. We can write code like the following: function foo(SomeInterface $i) { // ... call $i->someMethod(); } Within foo, all we know is that $i is an instance of SomeInterface. We can call any method on $i that SomeInterface defines. If one of those methods had a never argument, then we could never call it. If we did want to call the method with a never argument, then we'd need to know a specific subclass and what type it overrides never with. At that point, we're no longer using the interface; we're instead using the specific subclass, entirely eliminating the need for the interface in the first place.
> So I would ask - if not `never` parameters, then what should users do? It > seems like, for the use case of "the interface adds a method but there are > no promises about what parameters it accepts" >
If an interface adds a method but makes no promises about what parameters it accepts, then why is it part of the interface in the first place--why add a method that can't be used? Best regards, --Matthew

Tim Düsterhus

1 year ago
Hi Am 2025-03-20 21:27, schrieb Matt Fonda:
> If an interface adds a method but makes no promises about what > parameters > it accepts, then why is it part of the interface in the first > place--why > add a method that can't be used?
It would more cleanly allow for userland / PHPDoc-based generics, while still providing some engine-enforced type safety. Consider this example (not sure if I got the syntax completely right): /** @template T */ interface Comparable { /** @param T $other */ public function compareTo(never $other): int; } /** @implements Comparable<Number> */ final class Number implements Comparable { public function compareTo(Number $other): int { return $this <=> $other; } } Without `never`, the `$other` parameter in the interface would need to be `mixed` or untyped, preventing the method in the `Number` class from adding the type to the engine-enforced signature, requiring it to check manually inside the method body. To me this is another good example of how a small engine change can improve the safety of the language for all users, even when third party static analysis tools are required to make full use of it. Best regards Tim Düsterhus

Rowan Tommins [IMSoP]

1 year ago
On 21/03/2025 11:50, Tim Düsterhus wrote:
> Am 2025-03-20 21:27, schrieb Matt Fonda: >> If an interface adds a method but makes no promises about what >> parameters >> it accepts, then why is it part of the interface in the first place--why >> add a method that can't be used? > > It would more cleanly allow for userland / PHPDoc-based generics, > while still providing some engine-enforced type safety. Consider this > example (not sure if I got the syntax completely right): > >     /** @template T */ >     interface Comparable { >         /** @param T $other */ >         public function compareTo(never $other): int; >     } > >     /** @implements Comparable<Number> */ >     final class Number implements Comparable { >         public function compareTo(Number $other): int { return $this > <=> $other; } >     }
I think I agree with Matt on this: the interface isn't making any usable promises about that method. In this example, Comparable is a kind of "abstract interface" - in order to actually make use of it, you need to specialise it. Declaring that a class implements a template interface is like inheriting an abstract method: either you fill in the type parameter ("class Foo implements Comparable<Foo> { ... }"), or the class is also a template ("class Foo<A> implements Comparable<A> { ... }") I don't think the language should pretend to support something that it doesn't - if the contract is actually enforced by a third-party tool reading docblocks, put the contract in a docblock:     /**        * @template T        * @method compareTo(T $other): int;        */     interface Comparable {     }     /** @implements Comparable<Number> */     final class Number implements Comparable {         public function compareTo(Number $other): int { return $this <=> $other; }     }
-- Rowan Tommins [IMSoP]

Daniel Scherzer

1 year ago
On Tue, Mar 25, 2025 at 11:01 AM Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> wrote:
> > I don't think the language should pretend to support something that it > doesn't >
I don't see what the pretending is here - the engine supports declaring that a method must accept a parameter but makes no promises about the type of that parameter, which is what we claim it supports. If people want to use this for generics, then great, but I don't think this should be blocked on the expectation that it is only useful for userland generics (e.g. the Serde use case isn't really generics).
> - if the contract is actually enforced by a third-party tool reading > docblocks, put the contract in a docblock: > > /** > * @template T > * @method compareTo(T $other): int; > */ > interface Comparable { > } > > /** @implements Comparable<Number> */ > final class Number implements Comparable { > public function compareTo(Number $other): int { return $this <=> > $other; } > } > > > What happens if there is a bigger contract than just the never parameter,
and we want the engine to enforce the rest? For example, the Serde use case given previously. Or, if you had an object store that might occasionally clean up expired items: /** @template T */ interface ObjectStore { /** @param T $object */ public function storeObject(never $object, bool $alsoDoCleanup): void; } If we move the entire contract to the docblock, then the engine cannot be used to enforce the non-never-related parts. --Daniel

Rowan Tommins [IMSoP]

1 year ago
On 25 March 2025 18:14:21 GMT, Daniel Scherzer <daniel.e.scherzer@gmail.com> wrote:
>On Tue, Mar 25, 2025 at 11:01 AM Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> >wrote: > >> >> I don't think the language should pretend to support something that it >> doesn't >> > >I don't see what the pretending is here - the engine supports declaring >that a method must accept a parameter but makes no promises about the type >of that parameter
I guess I just struggle to make such a declaration have any meaning; it feels like a philosophical statement rather than a practical one. Perhaps "pretend" was too strong in this case; I was mentally comparing it to Python's unenforced type hints, which are only as reliable as comments; and the occasional suggestion of doing the same in PHP with generic declarations (built-in syntax with no built-in behaviour, which would be a hard No from me). If I assert($foo instanceof InterfaceUsingNever), I can't actually do anything useful with the promised method; I can only call it if I either read the unenforced rules (e.g. in docblocks and comments), or assert($foo instanceof SomeConcreteImplementation) instead. In the context of this thread, I can see that the examples are logically consistent, but if I just saw one in the wild, I'd just be scratching my head what it means to require that no values are acceptable. And I've seen plenty of junior devs struggle with simple things like the difference between "nullable" and "optional", so I worry that making the type system too rich will lose PHP its reputation as an approachable language. It's possible I'd react less strongly if a keyword was chosen that made more sense in context, although I'm not sure what that would be. public function foo(int $a, must_override $b); That's not quite right, I think. Rowan Tommins [IMSoP]

Tim Düsterhus

1 year ago
Hi Am 2025-03-25 21:42, schrieb Rowan Tommins [IMSoP]:
> And I've seen plenty of junior devs struggle with simple things like > the difference between "nullable" and "optional", so I worry that > making the type system too rich will lose PHP its reputation as an > approachable language. > > > It's possible I'd react less strongly if a keyword was chosen that made > more sense in context, although I'm not sure what that would be.
FWIW: There is precedent for “never” as a parameter type in TypeScript. In fact, the Comparable example I gave also works in TypeScript in an unmodified fashion: https://www.typescriptlang.org/play/?#code/JYOwLgpgTgZghgYwgAgMIHsC2AHOU4BGANigN4BQyVyCWuUEAKugBTpgAW0AXMiBADdoASl4gArpgLRyAX3LkEROAGcVyALIBPAHKTpUZMBwlMEcOow48hEsgrUadPE1bsuUXtr1SRY-dD2lI7UwDDIbJyBADzInMAqwkEhKcgMYOJQIMgAtACMwSmyyBBEKihhEe6BAHxxHAlJDqmO6ZnZBS3FpeXJLdRtWcgADIUh8o7y8uQw4iAIYMDo2TDorAi8VvS2EAA0yAAmm874xBBNY7QgKugkAHRE6ADmLAh3tNYMzCwHyKp8ghEwjkQA The `compareTo()` method is indeed not callable OOTB and I need to tell the compiler that I know what I'm doing by the `as never` assertion, but it still guarantees me that the return value is a number. I also expect folks to rarely encounter `never` as a parameter type in practice since generic interfaces are comparatively rare, but in the cases where you have a generic interface, `never` feels more useful to have than not having it. Best regards Tim Düsterhus

Matthew Fonda

1 year ago
Hi Tim and Larry, Thanks for sharing examples. I'm not sure I follow how never parameters help in either of these cases. As far as I can tell, the problem remains that these methods can't actually be called. On Fri, Mar 21, 2025 at 4:50 AM Tim Düsterhus <tim@bastelstu.be> wrote:
> Am 2025-03-20 21:27, schrieb Matt Fonda: > > If an interface adds a method but makes no promises about what > > parameters > > it accepts, then why is it part of the interface in the first > > place--why > > add a method that can't be used? > > It would more cleanly allow for userland / PHPDoc-based generics, while > still providing some engine-enforced type safety. Consider this example > (not sure if I got the syntax completely right): > > /** @template T */ > interface Comparable { > /** @param T $other */ > public function compareTo(never $other): int; > } > > /** @implements Comparable<Number> */ > final class Number implements Comparable { > public function compareTo(Number $other): int { return $this <=> > $other; } > }
I don't follow why Number would implement Comparable in the first place, since we won't actually be able to call Comparable::compareTo. i.e. we cannot write the following method: function greaterThan(Comparable $a, Comparable $b): bool { return $a->compareTo($b) > 0; } If we wanted to actually call compareTo, we need to use a specific implementation: function greaterThan(Number $a, Number $b): bool { return $a->compareTo($b) > 0; } At this point, we're no longer using the interface, so there's no point in Number implementing it. Number is then free to define compareTo(Number $other). I share Nikita's sentiment in the previous RFC discussion [1], and have yet to see an answer to it: I don't think this really addresses my concern, so let me repeat it: You
> cannot actually call a method using a never-type argument while typing > against the interface. What's the point of the interface then? > > I don't think "you must use this in conjunction with a 3rd-party phpdoc > generics implementation for it to make any sense at all" is a suitable way > to resolve that.
On Fri, Mar 21, 2025 at 8:53 AM Larry Garfield <larry@garfieldtech.com> wrote:
> Changing the interface to use `never` instead of `mixed` would have the > weakest guarantees of the three, since it doesn't force me to use the > *same* widened type on serializeInt(), serializeFloat(), serializeString(), > etc., even though it would always be the same. But it would allow me to > communicate more type information than I can now. >
Suppose you made this change from mixed to never. As long as you're typing against the Formatter interface and not a specific implementation, then you cannot actually call $formatter->serializeInt() etc. because the interface defines the parameter type as never, and you cannot call a method with a parameter type of never. This is the case in your real world usage [1]. Here, $serializer->formatter is typed against Formatter [2], not a specific implementation. As such, all we know is that we have an instance of Formatter--we don't know anything about the concrete type--and thus we can't actually call e.g. serializeInt() because never is the only type we know here. Best regards, --Matthew [1] https://externals.io/message/115712#115752 [2] https://github.com/Crell/Serde/blob/777fc16e932d4dcf1d600335961685885cd815c4/src/PropertyHandler/ScalarExporter.php#L17 [3] https://github.com/Crell/Serde/blob/777fc16e932d4dcf1d600335961685885cd815c4/src/Serializer.php#L31

Larry Garfield

1 year ago
On Fri, Mar 28, 2025, at 3:42 PM, Matt Fonda wrote:
> Hi Tim and Larry, > > Thanks for sharing examples. I'm not sure I follow how never parameters > help in either of these cases. As far as I can tell, the problem > remains that these methods can't actually be called.
> I share Nikita's sentiment in the previous RFC discussion [1], and have > yet to see an answer to it: > >> I don't think this really addresses my concern, so let me repeat it: You >> cannot actually call a method using a never-type argument while typing >> against the interface. What's the point of the interface then? >> >> I don't think "you must use this in conjunction with a 3rd-party phpdoc >> generics implementation for it to make any sense at all" is a suitable way >> to resolve that. > > On Fri, Mar 21, 2025 at 8:53 AM Larry Garfield <larry@garfieldtech.com> wrote: >> Changing the interface to use `never` instead of `mixed` would have the weakest guarantees of the three, since it doesn't force me to use the *same* widened type on serializeInt(), serializeFloat(), serializeString(), etc., even though it would always be the same. But it would allow me to communicate more type information than I can now. > > Suppose you made this change from mixed to never. As long as you're > typing against the Formatter interface and not a specific > implementation, then you cannot actually call > $formatter->serializeInt() etc. because the interface defines the > parameter type as never, and you cannot call a method with a parameter > type of never. > > This is the case in your real world usage [1]. Here, > $serializer->formatter is typed against Formatter [2], not a specific > implementation. As such, all we know is that we have an instance of > Formatter--we don't know anything about the concrete type--and thus we > can't actually call e.g. serializeInt() because never is the only type > we know here.
I have to think people are misunderstanding Nikita's earlier comment, or perhaps that he phrased it poorly. The determination of whether a method call is type-compatible with the parameters passed to it is made *at runtime*, on the class. You can widen a parameter type in an implementing class, and it will work. That's been the case since PHP 7.4. For example: https://3v4l.org/5YPdg Even though the function is typed against I, if it's passed a C, you can call it with a string. That's because C::a()'s param type is wider than the interface. The idea of a never typed parameter is exactly the same: It starts off super-narrow (accepts nothing), so implementations can accept anything wider than "nothing" and still be type compatible. You *can* call it. What you cannot do is determine *statically* that it is callable, because at static-analysis time, all you know is the interface. So SA tools won't be able to verify that anything is valid for that interface. That's a valid criticism of never params, I agree. Is it enough to vote against it on those grounds? That's up to each voter to decide. But "you cannot ever even call it" is simply not true, unless there's some weird engine limitation that I don't know about. In fairness, though, it seems to me that Associated Types would have the same SA issue. It would probably be more evident for them that they cannot try to enforce the type statically, but I don't know how they could do a better job of reporting it than with a never type. (Someone else who understands Associated Types better than I, how would that work?) --Larry Garfield

Matthew Fonda

1 year ago
Hi Larry, On Fri, Mar 28, 2025 at 7:48 PM Larry Garfield <larry@garfieldtech.com> wrote:
> I have to think people are misunderstanding Nikita's earlier comment, or > perhaps that he phrased it poorly. > > The determination of whether a method call is type-compatible with the > parameters passed to it is made *at runtime*, on the class. You can widen > a parameter type in an implementing class, and it will work. That's been > the case since PHP 7.4. > > For example: https://3v4l.org/5YPdg > > Even though the function is typed against I, if it's passed a C, you can > call it with a string. That's because C::a()'s param type is wider than > the interface. > > The idea of a never typed parameter is exactly the same: It starts off > super-narrow (accepts nothing), so implementations can accept anything > wider than "nothing" and still be type compatible. You *can* call it. > > What you cannot do is determine *statically* that it is callable, because > at static-analysis time, all you know is the interface. So SA tools won't > be able to verify that anything is valid for that interface. That's a > valid criticism of never params, I agree. Is it enough to vote against it > on those grounds? That's up to each voter to decide. > > But "you cannot ever even call it" is simply not true, unless there's some > weird engine limitation that I don't know about. >
Correct, in saying you can't call it, we're referring to a static analysis perspective--or phrased differently, a "this is why interfaces exist and how you correctly use them" perspective. If we're writing code against an interface, the only thing we're "allowed" to do with a method is exactly what the interface specifies. If the interface specifies we can never call a method, then we can... never call it. In other words, we can't write code against the interface, so what's the point of the interface? It doesn't provide any extra safety. In fact, quite the opposite. Any code written against a method with a `never` parameter is inherently unsafe--it only works if we happen to pass the correct types at runtime. Widening from something (as opposed to widening from nothing, i.e. `never`) to something wider (e.g. `int` to `int|string` in your example) makes sense in a way that widening `never` does not. In this case, you have an actual type to begin with, so you can still write code against the interface. Continuing your example, if we're writing code against I::a(), the only thing we're "allowed" to do with it is call it with an int (otherwise the code may fail at runtime, e.g. https://3v4l.org/WUTv4). However, being able to widen here is still useful, because we can _also_ write code against C::a(), and in that context we're allowed to call with an int or a string. See https://3v4l.org/qMZOH. Contrast this to never parameters, where we were never able to write code against the interface. I'd argue that this is certainly grounds to vote against it--there's no point in using an interface if we can't write code against it. Best regards, --Matthew

Daniel Scherzer

1 year ago
On Mon, Mar 10, 2025 at 12:05 PM Daniel Scherzer < daniel.e.scherzer@gmail.com> wrote:
> Hi internals, > > I'd like to start discussion on a new RFC about allowing `never` for > parameter types when declaring a method. > > * RFC: https://wiki.php.net/rfc/never-parameters-v2 > * Implementation: https://github.com/php/php-src/pull/18016 > > -Daniel >
Since a lot of the discussion seems to be around static analysis and whether there is a real use case for this, I wanted to share another use case I just came across: in the `thephpleague/commonmark` package, different renderers are added to render different types (subclasses) of `League\CommonMark\Node\Node`. You can see the interface for renderers at [1]. The overall interface supports being called with any `Node` type, but each underlying renderer expects to be called with a *narrower* type than that. To avoid LSP violations, the renderers * have a `Node` typehint * have a documentation comment with the actual subclass of `Node` that they support * manually throw an exception on invalid values See, e.g., the default renderer for paragraphs[2]. This seems like exactly the place where you would find it useful to have `never` parameters. The current implementation * uses comments and static analysis tools to document the restriction * manually throws an exception when violated Whereas if the base class had a `never` parameter, it could * use language typehints to document the restriction * have the exception enforced automatically I don't think we should be worried about the fact that, under static analysis, we don't know what type of value is accepted for a `never` parameter, because under actual operation, you can always just manually throw an exception for a type you don't want, like commonmark does. -Daniel [1] https://github.com/thephpleague/commonmark/blob/2.6/src/Renderer/NodeRendererInterface.php [2] https://github.com/thephpleague/commonmark/blob/2.6/src/Renderer/Block/ParagraphRenderer.php

Daniel Scherzer

1 year ago
On Tue, Apr 8, 2025 at 6:40 PM Daniel Scherzer <daniel.e.scherzer@gmail.com> wrote:
> > Since a lot of the discussion seems to be around static analysis and > whether there is a real use case for this, I wanted to share another use > case I just came across: in the `thephpleague/commonmark` package, > different renderers are added to render different types (subclasses) of > `League\CommonMark\Node\Node`. >
I added this example to the RFC page as an example of how this could be useful in userland code. Barring further developments, I plan to open the RFC for voting in a few days. -Daniel

Andreas Hennings

1 year ago
On Tue, 15 Apr 2025 at 20:59, Daniel Scherzer <daniel.e.scherzer@gmail.com> wrote:
> > On Tue, Apr 8, 2025 at 6:40 PM Daniel Scherzer <daniel.e.scherzer@gmail.com> wrote: >> >> >> Since a lot of the discussion seems to be around static analysis and whether there is a real use case for this, I wanted to share another use case I just came across: in the `thephpleague/commonmark` package, different renderers are added to render different types (subclasses) of `League\CommonMark\Node\Node`. > > > > I added this example to the RFC page as an example of how this could be useful in userland code. Barring further developments, I plan to open the RFC for voting in a few days. > > -Daniel >
Thank you, I have been wanting this since a long time! https://externals.io/message/100275 ---- One interesting application is intersection types. https://externals.io/message/115712#123276 The intersection of different types with "never" parameters and "mixed" return types naturally produces a valid new type: (function(A, never): mixed) & (function(never, B): mixed) & (function(never, never): R) === function (A, B): R (imagine all of the above to be interfaces) ---- An interesting question I wonder is whether we would ever want "constrained never" parameters. E.g. currently we can have an interface I with function (A&B), where A and B are classes/interfaces. A sub-interface J that extends I can have function (A) or function (B) or function (mixed) or function (A|SomethingElse). But it cannot have function (C), if neither A nor B extends C. In the same way, for function(int&string), a child method would have to allow at least int or string, e.g. it cannot be function(float) or function(C). (it could be extended as function(int|float) or function(mixed), but not function(float).) So in a way this could be a more suitable parameter type for BackedEnum::from() and ::tryFrom(). For the main part of this RFC we do not need to worry about this. For the BackedEnum::from() and ::tryFrom(), if we change the type to 'never' now, we can no longer change it to int&string later without breaking BC. ---- Another thing I wonder is whether the "Proposal" section needs to more explicitly define the behavior. Can we rely on the "Introduction" part and the examples in other sections, or does the "Proposal" part need to be complete and sufficient by itself? What would we add, and what would be redundant? "A method with a never parameter cannot be called." This already follows from "it cannot be used in the declaration of any method that has an implementation.", so we do not need to explicitly add it. "The never parameter type can only be used standalone, not as part of a union or intersection or nullable type". This might already be specified elsewhere, where never is defined as a return type. "A child class or interface that overrides a method with a never parameter can replace the never parameter type with any other type, or omit the parameter type altogether." This is obvious to us from LSP, but do we need to explicitly define it? It is currently mentioned in "Introduction" but not in "Proposal". "A child class or interface that overrides a method with a never parameter must respect LSP for the other parameters." So, you cannot extend a function(A, never) with function(never, never). I think this is pretty clear from general rules about covariance/contravariance. So the only point that might make sense to add is the third one about widening the type. -- Andreas