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
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
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
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
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
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
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
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
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
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
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 viewfrom
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
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
andtryFrom
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
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
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
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]
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
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'tI 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]
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:
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
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.
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 ofmixed
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
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.Changing the interface to use
never
instead ofmixed
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
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
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
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 thethephpleague/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
On Tue, 15 Apr 2025 at 20:59, 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) ofLeague\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