[PHP-DEV] [RFC] [Discussion] Never parameters

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&lt;Number&gt; \*/
 final class Number implements Comparable \{
     public function compareTo\(Number $other\): int \{ return $this &lt;=&gt; $other; \}
 \}

--
Rowan Tommins
[IMSoP]

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

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. :slight_smile:

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

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 */
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 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]

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. :slight_smile:

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

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.

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 */
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

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: Online PHP editor | output for 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.

-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 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

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!

----

One interesting application is intersection types.

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