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 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
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:
- 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. - 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
Hi Daniel,
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
[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.
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.
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.
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
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
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
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
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
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
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:
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