# [RFC] Never For Argument Types

- Thread: https://externals.io/message/115712
- Original archive: https://news-web.php.net/php.internals/115712
- Started by Jordan LeDoux on 2021-08-13T23:27:12+00:00
- Messages: 29
- Page 1 of 2: messages 1 to 25 (25 per page, add `?page=N` to the URL)
- Next page: https://externals.io/message/115712.md?page=2
- Reply by email to internals@lists.php.net
- More for agents: search with https://externals.io/search.md?q={query} · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

---

## #115712 — Jordan LeDoux — 2021-08-13T23:27:12+00:00

URL: https://externals.io/message/115712#115712

Hey internals,

I've been working on the draft for my operator overloading RFC, and in
doing so I encountered a separate change that I would like to see.

That is, the use of `never` as an argument type for interfaces. Since
arguments in PHP are contravariant to preserve Liskov substitution, `never`
as the bottom type should indicate that implementing classes can require
any type combination they want. This is in fact consistent with type theory
and set theory, and is how the bottom type is treated in several other
languages.

In this case, the bottom type would be used to indicate covariant parameter
polymorphism while not conflicting with LSP.

This would provide a sort of minimal form of generics to PHP without the
issues that actual generics present from an implementation perspective. It
would not, however, restrict or hinder any future RFC for generics.

This is at the first draft stage, and I currently have the RFC on a github
repo to allow for easy contribution and collaboration.

Any feedback is greatly appreciated.

https://github.com/JordanRL/never-argument-type

Jordan

---

## #115713 — Deleu — 2021-08-14T08:39:17+00:00

URL: https://externals.io/message/115712#115713 · In reply to: #115712

Hi Jordan,

Does it make sense to explain in the RFC the difference between never and
mixed in this context? The RFC vaguely mentions that never can never be
used directly, but if it's limited to abstract class and interfaces, isn't
that already impossible to use directly? Or does it mean that the type
would "be allowed" on any function, but because of its intrinsic behavior
it would always fail if used in non-abstract methods?

Another clarification I'd be interested is about dropping the type
declaration entirely (e.g. https://3v4l.org/a4bfs), because of covariance
(or contravariance, I never know), sub classes can completely drop type
declaration entirely. Will never not allow this? Why? Why not? If it does
allow this, does it really differ from not having any type declaration on
the abstract function?

My knowledge in this area is practically null so if I'm asking stupid
questions that are easily explained by some blog post I'd he happy to read
it.



> Hey internals,
>
> I've been working on the draft for my operator overloading RFC, and in
> doing so I encountered a separate change that I would like to see.
>
> That is, the use of `never` as an argument type for interfaces. Since
> arguments in PHP are contravariant to preserve Liskov substitution, `never`
> as the bottom type should indicate that implementing classes can require
> any type combination they want. This is in fact consistent with type theory
> and set theory, and is how the bottom type is treated in several other
> languages.
>
> In this case, the bottom type would be used to indicate covariant parameter
> polymorphism while not conflicting with LSP.
>
> This would provide a sort of minimal form of generics to PHP without the
> issues that actual generics present from an implementation perspective. It
> would not, however, restrict or hinder any future RFC for generics.
>
> This is at the first draft stage, and I currently have the RFC on a github
> repo to allow for easy contribution and collaboration.
>
> Any feedback is greatly appreciated.
>
> https://github.com/JordanRL/never-argument-type
>
> Jordan

---

## #115715 — G. P. B. — 2021-08-14T12:48:25+00:00

URL: https://externals.io/message/115712#115715 · In reply to: #115713

> Hi Jordan,
>
> Does it make sense to explain in the RFC the difference between never and
> mixed in this context? The RFC vaguely mentions that never can never be
> used directly, but if it's limited to abstract class and interfaces, isn't
> that already impossible to use directly? Or does it mean that the type
> would "be allowed" on any function, but because of its intrinsic behavior
> it would always fail if used in non-abstract methods?
>
> Another clarification I'd be interested is about dropping the type
> declaration entirely (e.g. https://3v4l.org/a4bfs), because of covariance
> (or contravariance, I never know), sub classes can completely drop type
> declaration entirely. Will never not allow this? Why? Why not? If it does
> allow this, does it really differ from not having any type declaration on
> the abstract function?
>
> My knowledge in this area is practically null so if I'm asking stupid
> questions that are easily explained by some blog post I'd he happy to read
> it.
>

never and mixed are on opposite sides of the type hierarchy.
mixed is the top type, meaning it's the supertype of any other types, and
any other type is a subtype of mixed (excluding void which is not really a
"type" if you look at it, from my understanding, in a type theory way).
never on the other side is the bottom type, meaning it's the subtype of any
other type, and any other type is a supertype of never.
Finally a lack of type declaration is treated as mixed.

Liskov's substitutions rules dictate that return types are co-variant i.e.
more specific, and argument types are contra-variant i.e. more general.
This is why any return type can be replaced by never and any argument type
can have it's type dropped/changed to mixed.
As such replacing never by mixed in an argument is totally possible as
mixed is a wider type than never.

How I personally see never as an argument type is that you require a
mandatory argument but you leave the type constraint up to the
implementation.

A recent example which bit us in php-src/the PHP documentation is the
interface of ArrayAccess, all of the $offset parameters have a mixed type.
Until recently the ArrayObject/ArrayIterator had incorrect stubs [1] by
indicating that the argument was of type int|string, which practically is
the case as SPL's ArrayAccess handler will throw a TypeError on different
types, however this is done manually within the call and not when the call
is made.
Ideally the $offset parameter would be of type never such that SPL, and
userland, can specify what type of offsets they accept, be that the usual
int|string, only int for list-like objects, only string for dictionary-like
objects, maybe even object|int|string to allow GMP objects for arbitrary
precision.
Whereas every implementer of ArrayAccess is forced to accept mixed for the
$offset and need to manually enforce the type.

This is the "power" of the never type as an argument type.

Best regards,

George P. Banyard

[1]
https://github.com/php/php-src/pull/7215/files#diff-e330df347cac9e68d2d07a06535c534cd8c2438a1af703cd4245b8ce91ec65afL9-R10

---

## #115716 — Larry Garfield — 2021-08-14T13:04:33+00:00

URL: https://externals.io/message/115712#115716 · In reply to: #115715

> 
> 
> > […]
> 
> never and mixed are on opposite sides of the type hierarchy.
> mixed is the top type, meaning it's the supertype of any other types, and
> any other type is a subtype of mixed (excluding void which is not really a
> "type" if you look at it, from my understanding, in a type theory way).
> never on the other side is the bottom type, meaning it's the subtype of any
> other type, and any other type is a supertype of never.
> Finally a lack of type declaration is treated as mixed.
> 
> Liskov's substitutions rules dictate that return types are co-variant i.e.
> more specific, and argument types are contra-variant i.e. more general.
> This is why any return type can be replaced by never and any argument type
> can have it's type dropped/changed to mixed.
> As such replacing never by mixed in an argument is totally possible as
> mixed is a wider type than never.
> 
> How I personally see never as an argument type is that you require a
> mandatory argument but you leave the type constraint up to the
> implementation.
> 
> A recent example which bit us in php-src/the PHP documentation is the
> interface of ArrayAccess, all of the $offset parameters have a mixed type.
> Until recently the ArrayObject/ArrayIterator had incorrect stubs [1] by
> indicating that the argument was of type int|string, which practically is
> the case as SPL's ArrayAccess handler will throw a TypeError on different
> types, however this is done manually within the call and not when the call
> is made.
> Ideally the $offset parameter would be of type never such that SPL, and
> userland, can specify what type of offsets they accept, be that the usual
> int|string, only int for list-like objects, only string for dictionary-like
> objects, maybe even object|int|string to allow GMP objects for arbitrary
> precision.
> Whereas every implementer of ArrayAccess is forced to accept mixed for the
> $offset and need to manually enforce the type.
> 
> This is the "power" of the never type as an argument type.
> 
> Best regards,
> 
> George P. Banyard
> 
> [1]
> https://github.com/php/php-src/pull/7215/files#diff-e330df347cac9e68d2d07a06535c534cd8c2438a1af703cd4245b8ce91ec65afL9-R10

So... if I am following correctly, the idea is to allow `never` to be used in an interface/abstract method only, as a way to indicate "you must specify a type here of some kind; I don't care what, even mixed, but you have to put something".  Am I following?

I'm not sure on the type theory of it, but it does feel like a hack at first blush.

--Larry Garfield

---

## #115717 — Deleu — 2021-08-14T13:12:46+00:00

URL: https://externals.io/message/115712#115717 · In reply to: #115715

> 
>
> > […]
>
> never and mixed are on opposite sides of the type hierarchy.
> mixed is the top type, meaning it's the supertype of any other types, and
> any other type is a subtype of mixed (excluding void which is not really a
> "type" if you look at it, from my understanding, in a type theory way).
> never on the other side is the bottom type, meaning it's the subtype of
> any other type, and any other type is a supertype of never.
> Finally a lack of type declaration is treated as mixed.
>
> Liskov's substitutions rules dictate that return types are co-variant i.e.
> more specific, and argument types are contra-variant i.e. more general.
> This is why any return type can be replaced by never and any argument type
> can have it's type dropped/changed to mixed.
> As such replacing never by mixed in an argument is totally possible as
> mixed is a wider type than never.
>

This was incredibly useful, thank you very much. The essence that I took
away is that if I declare mixed on my interface, then making it more
specific (co-variant) is impossible e.g.

```
interface A {
    public function t(mixed $t);
}


interface B extends A {
    public function t(A $t);
}
```

This is where never, being a bottom type, shines.

I guess the bike shed of this one is the awkwardness of declaring an input
as `never` but I'd imagine we'll either get used to it or use a possible
future type alias to do `type something = never`. I guess the best aliases
would be any or something, but given the interesting use case I don't think
it's worth to reject this RFC solely based on how awkward `never $var` is.

TL;DR thank you and I like it.

---

## #115719 — Nikita Popov — 2021-08-14T13:25:36+00:00

URL: https://externals.io/message/115712#115719 · In reply to: #115712

On Sat, Aug 14, 2021 at 1:27 AM Jordan LeDoux <jordan.ledoux@gmail.com>
wrote:

> Hey internals,
>
> I've been working on the draft for my operator overloading RFC, and in
> doing so I encountered a separate change that I would like to see.
>
> That is, the use of `never` as an argument type for interfaces. Since
> arguments in PHP are contravariant to preserve Liskov substitution, `never`
> as the bottom type should indicate that implementing classes can require
> any type combination they want. This is in fact consistent with type theory
> and set theory, and is how the bottom type is treated in several other
> languages.
>
> In this case, the bottom type would be used to indicate covariant parameter
> polymorphism while not conflicting with LSP.
>
> This would provide a sort of minimal form of generics to PHP without the
> issues that actual generics present from an implementation perspective. It
> would not, however, restrict or hinder any future RFC for generics.
>
> This is at the first draft stage, and I currently have the RFC on a github
> repo to allow for easy contribution and collaboration.
>
> Any feedback is greatly appreciated.
>
> https://github.com/JordanRL/never-argument-type
>

There's two sides to this coin: While using a never argument type allows
you to avoid the LSP problem, it also means that you cannot call the method
while typing against the interface. Let's take your CollectionInterface

interface CollectionInterface {
    public function add(never $input): self;
}

and actually try to make use of it:

function addMultiple(CollectionInterface $collection, mixed ...$inputs):
void {
    foreach ($inputs as $input) $collection->add($input);
}

A static analyzer should flag this CollectionInterface::add() call as
invalid, because mixed is passed to never. Effectively, this means that an
interface using never argument types cannot actually be used in anything
*but* inheritance -- so what is its purpose?

Compare this to generics. The interface changes to

interface CollectionInterface<T> {
    public function add(T $input): self;
}

and the use changes to

function addMultiple<T>(CollectionInterface<T> $collection, T ...$inputs):
void {
    foreach ($inputs as $input) $collection->add($input);
}

Now a T argument is passed to a T parameter, and everything is fine.

Regards,
Nikita

---

## #115720 — Jordan LeDoux — 2021-08-14T13:43:55+00:00

URL: https://externals.io/message/115712#115720 · In reply to: #115719

>
> function addMultiple(CollectionInterface $collection, mixed ...$inputs):
> void {
>     foreach ($inputs as $input) $collection->add($input);
> }
>
> A static analyzer should flag this CollectionInterface::add() call as
> invalid, because mixed is passed to never. Effectively, this means that an
> interface using never argument types cannot actually be used in anything
> *but* inheritance -- so what is its purpose?
>
>
When used as a sort of... pseudo-generics replacement, you'd need to use
Docblocks to specify these, because **this feature is not generics** (which
you correctly pointed out). I probably should have made that MORE clear so
as to not confuse or trick anyone.

If this RFC were passed, it could be sort of used like generics but it
would be a bit hacky to use it that way as your example illustrates. In the
absence of generics, this would probably be used as a stopgap in
combination with docblocks. That's the point I was trying to make. :)

The main value I see from an inheritance perspective is using never to
disallow an omitted type. The inheriting class may specify *any* type, even
mixed, but it must do so explicitly.

Larry:
> So... if I am following correctly, the idea is to allow `never` to be
used in an interface/abstract method only, as a way to indicate "you must
specify a type here of some kind; I don't care what, even mixed, but you
have to put something".  Am I following?

This is essentially correct, yes, however it's important to note, and I
don't want to mislead anyone here: it's not possible (to my knowledge) to
*only* allow this type for parameters on interfaces and abstracts. Or at
least, doing so is much, much more complicated. The patch I provided is
about 5 lines different, however it allows never as a parameter type
everywhere, including functions.

This is, to my mind, acceptable because never will behave entirely
consistently with a bottom type in all such scenarios. It will compile just
fine, but if you call any function that has an argument type of never, you
will get TypeError. No type which can be provided at runtime, even null,
will satisfy the type never, so practically it makes a function uncallable
when used as an argument type. It must be widened through inheritance to be
used.

Jordan

---

## #115721 — AllenJB — 2021-08-14T14:01:10+00:00

URL: https://externals.io/message/115712#115721 · In reply to: #115712

> Hey internals,
>
> I've been working on the draft for my operator overloading RFC, and in
> doing so I encountered a separate change that I would like to see.
>
> That is, the use of `never` as an argument type for interfaces. Since
> arguments in PHP are contravariant to preserve Liskov substitution, `never`
> as the bottom type should indicate that implementing classes can require
> any type combination they want. This is in fact consistent with type theory
> and set theory, and is how the bottom type is treated in several other
> languages.
>
> In this case, the bottom type would be used to indicate covariant parameter
> polymorphism while not conflicting with LSP.
>
> This would provide a sort of minimal form of generics to PHP without the
> issues that actual generics present from an implementation perspective. It
> would not, however, restrict or hinder any future RFC for generics.
>
> This is at the first draft stage, and I currently have the RFC on a github
> repo to allow for easy contribution and collaboration.
>
> Any feedback is greatly appreciated.
>
> https://github.com/JordanRL/never-argument-type
>
> Jordan

Whilst I understand there's a historical aspect to the keyword naming 
used in this RFC, as a PHP developer, I think the use of "never" here is 
going to be confusing to people when considered along-side the never 
return type.

I think it would make more sense to change the keyword for this RFC to 
something else such as "any", "unspecified", "unknown" or "specify". 
(Whilst "nothing" might also be considered given its use in other 
languages, I don't think the meaning / purpose is that clear).

This RFC only considers this feature for arguments. Why is it also not a 
valid feature for return types? I think it might be useful for abstract 
classes and interfaces (for example, the Iterator interface) to force 
implementers to specify a return type for certain methods whilst not 
specifying anything about it themselves.

---

## #115723 — G. P. B. — 2021-08-14T14:39:31+00:00

URL: https://externals.io/message/115712#115723 · In reply to: #115721

> This RFC only considers this feature for arguments. Why is it also not a
> valid feature for return types? I think it might be useful for abstract
> classes and interfaces (for example, the Iterator interface) to force
> implementers to specify a return type for certain methods whilst not
> specifying anything about it themselves.
>

You can already do this since PHP 7.4, because return types are co-variant.
Any implementor can add a return type to specify what they return, as mixed
is the top type.
This RFC is not about "forcing" X to declare a type, it is to allow
variance changes which cannot currently be done
(well except adding a dummy argument type of bool that you can widen to
something more general and just need to check the case is not bool but
that's also hackery).

This feature is for sure an edge case and not a replacement for generics,
but the main motivation I see is to be useful for engine hooks like a
Equatable/Ordable comparison overload or more general operator overloading,
where if the object doesn't specify it's capability it is like it
"implements" the magic method (or whatever it comes to be) with an argument
type of "never" which results in a TypeError meaning it does not support
this operation.

Maybe it does not make sense to make this available for userland, but I do
think it is somewhat necessary for these sorts of features to have this
concept.

Best regards,

George P. Banyard

---

## #115724 — Jordan LeDoux — 2021-08-14T14:40:43+00:00

URL: https://externals.io/message/115712#115724 · In reply to: #115721

>
> Whilst I understand there's a historical aspect to the keyword naming
> used in this RFC, as a PHP developer, I think the use of "never" here is
> going to be confusing to people when considered along-side the never
> return type.
>
> I think it would make more sense to change the keyword for this RFC to
> something else such as "any", "unspecified", "unknown" or "specify".
> (Whilst "nothing" might also be considered given its use in other
> languages, I don't think the meaning / purpose is that clear).
>
> This RFC only considers this feature for arguments. Why is it also not a
> valid feature for return types? I think it might be useful for abstract
> classes and interfaces (for example, the Iterator interface) to force
> implementers to specify a return type for certain methods whilst not
> specifying anything about it themselves.
>
>
Never is treated as the bottom type for the purpose of Liskov substitution
already with its use as a return type. The exception to this in its use as
a return type is that it isn't treated as the union identity in the return
type. However, for LSP never is the bottom type when it comes to return
values. It would, in my mind, be highly inconsistent to have a different
bottom type for return values than for arguments, so personally I am very
against using a term other than never. As mentioned in the doc, I wasn't
able to find a single existing language that has multiple bottom types. If
anyone is able to provide an example of this, I would appreciate it.

Never is the most common bottom type in other languages from the brief
survey I did before putting this RFC together, with "nothing" as the second
most common.

Return types are covariant with inheritance, not contravariant. The return
type equivalent from an inheritance perspective would be "mixed", as it is
the top type and is a supertype of any type in PHP, allowing inheriting
classes to narrow the type in any way they wish. The main difference is
that mixed is a valid type at runtime for values, since it contains all
valid values. Meanwhile, never contains no valid values, thus forcing type
expansion upon inheritance. I can see your point about how it might be
useful for interfaces to require an explicit return type of their
implementers, but that is a bit more complicated of a change (and a more
breaking one), and the point of this RFC isn't really to reorganize the
entire type system.

To retain Liskov substitution, the type to mirror this behavior for return
values would be mixed since that's the top type, and changing mixed so that
it required an explicit type of all inheriting classes would break quite a
bit of code. This RFC as it is written has no BC breaks.

Jordan

---

## #115726 — Jordan LeDoux — 2021-08-14T15:41:50+00:00

URL: https://externals.io/message/115712#115726 · In reply to: #115723

The RFC has been moved to the wiki:
https://wiki.php.net/rfc/never_for_parameter_types

The most critical points of discussion in my mind are:

1. Should never require **explicit** widening as discussed? This could be
very useful but would make it the only case in PHP where omitting a type is
not seen as "mixed". (This could be set up as a secondary vote.)
2. If the answer to question #1 is yes, should an error specific to this
case be provided instead of the more generic "declaration of A must be
compatible with B"?
3. Should we attempt to limit usage of this feature to only interfaces and
abstract classes? (NOTE: It's unclear how such a thing would be implemented
or if it is feasible.) If it must be available to all function parameters
due to implementation, is that acceptable?

To clarify, this feature is most critically useful for certain internally
provided interfaces, such as ArrayAccess or the proposed interfaces in my
draft of Operator Overloads. I'm sure it would find use in user code as
well (I know that I would use it in certain interfaces in my own
libraries), but it's absence actually makes certain core features very
difficult to provide in an intelligent way.

Jordan

---

## #115728 — Matthew Brown — 2021-08-14T16:19:30+00:00

URL: https://externals.io/message/115712#115728 · In reply to: #115712

Hey!

Using the "never" type to require that downstream libs specify a type does
not make intuitive sense to me, because the same is not true the other way
(covariantly) for return types.

The existence of a "never" type on an overriding method does not require
that upstream libs specify a return type — this is perfectly valid:

class A {
  public function foo() {}
}
class AChild extends A {
  public function foo():never { exit; }
}

Best wishes,

Matt



> Hey internals,
>
> I've been working on the draft for my operator overloading RFC, and in
> doing so I encountered a separate change that I would like to see.
>
> That is, the use of `never` as an argument type for interfaces. Since
> arguments in PHP are contravariant to preserve Liskov substitution, `never`
> as the bottom type should indicate that implementing classes can require
> any type combination they want. This is in fact consistent with type theory
> and set theory, and is how the bottom type is treated in several other
> languages.
>
> In this case, the bottom type would be used to indicate covariant parameter
> polymorphism while not conflicting with LSP.
>
> This would provide a sort of minimal form of generics to PHP without the
> issues that actual generics present from an implementation perspective. It
> would not, however, restrict or hinder any future RFC for generics.
>
> This is at the first draft stage, and I currently have the RFC on a github
> repo to allow for easy contribution and collaboration.
>
> Any feedback is greatly appreciated.
>
> https://github.com/JordanRL/never-argument-type
>
> Jordan

---

## #115729 — Claude Pache — 2021-08-14T17:12:56+00:00

URL: https://externals.io/message/115712#115729 · In reply to: #115728

> 
> 
> 
> > […]


> Le 14 août 2021 à 18:19, Matthew Brown <matthewmatthew@gmail.com> a écrit :
> 
> Hey!
> 
> Using the "never" type to require that downstream libs specify a type does
> not make intuitive sense to me, because the same is not true the other way
> (covariantly) for return types.
> 
> The existence of a "never" type on an overriding method does not require
> that upstream libs specify a return type — this is perfectly valid:
> 
> class A {
>  public function foo() {}
> }
> class AChild extends A {
>  public function foo():never { exit; }
> }
> 
> Best wishes,
> 
> Matt


Indeed, I was going to write something similar. Concretely, I assume that one would want to update the ArrayAccess internal interface as follows:

```php
interface ArrayAccess {
    public function offsetGet(never $x): mixed;
    // ...
}
```
If users of that interface would suddenly be *required* to specify a parameter type, whereas previously they were *forbidden* to specify one, except a meaningless `mixed`... it would be not nice and useless.

Moreover, note that `mixed` was only introduced very recently, in 8.0: so that, it would be impossible to implement `ArrayAccess` on code that work both in 7.x and a future version with the updated interface. Not only would it be not nice, but it would be positively harmful.


—Claude

---

## #115730 — Jordan LeDoux — 2021-08-14T17:21:18+00:00

URL: https://externals.io/message/115712#115730 · In reply to: #115729

On Sat, Aug 14, 2021 at 10:12 AM Claude Pache <claude.pache@gmail.com>
wrote:

>
> If users of that interface would suddenly be *required* to specify a
> parameter type, whereas previously they were *forbidden* to specify one,
> except a meaningless `mixed`... it would be not nice and useless.
>
> Moreover, note that `mixed` was only introduced very recently, in 8.0: so
> that, it would be impossible to implement `ArrayAccess` on code that work
> both in 7.x and a future version with the updated interface. Not only would
> it be not nice, but it would be positively harmful.
>
>
Those are some excellent points I hadn't considered. I will remove the
explicit widening from this RFC and update my pull request. I was
considering having that aspect as a separate vote, but this point about
mixed being a recent addition was a point I hadn't considered, and to me
suggests that it shouldn't even be an option in the RFC, particularly
because we would like to update the ArrayAccess interface and that will
affect a lot of existing code.

Jordan

---

## #115731 — Mike Schinkel — 2021-08-15T01:58:24+00:00

URL: https://externals.io/message/115712#115731 · In reply to: #115724

> 
> 
> Never is treated as the bottom type for the purpose of Liskov substitution
> already with its use as a return type. The exception to this in its use as
> a return type is that it isn't treated as the union identity in the return
> type. However, for LSP never is the bottom type when it comes to return
> values. It would, in my mind, be highly inconsistent to have a different
> bottom type for return values than for arguments, so personally I am very
> against using a term other than never. As mentioned in the doc, I wasn't
> able to find a single existing language that has multiple bottom types. If
> anyone is able to provide an example of this, I would appreciate it.

Reading through all the replies on this topic it seems that the functionality for the proposal is less controversial than the keyword chosen to identify it.  

It occurs to me that while `never` is the correct keyword for the bottom type given past decisions, maybe choosing to use a bottom type to provide this functionality is not an idea choice?

If we approach this use-case requirements from a perspective of "this keyword indicates that you must implement in a child" then I think we already have a keyword that has appropriate semantics compared with the confusing semantics of `never` used for a parameter: `abstract`.

So then instead of `never` we could choose the following:

     interface CollectionInterface
     {
         public function add(abstract $input): self;
     }

`abstract` could also work for return types and property types:

     interface Foo
    {
         public abstract $bar;
         public function baz(): abstract {}
    }

Or am I missing something?  Is there a reason `abstract` would not be a better choice than `never`?

================

That said, I do have a rhetorical question to ask, to be pondered by those who are better attuned to the ramifications of allowing interfaces to become less strict than they already are.

From my career-long understanding of declared interfaces the primary (only?) reason to use them is to signal and enforce a guarantee that a specific set of signatures are available in an instance of a class that implements the interface. But if an interface can be defined as something that can can easily change based on the implementor, the guarantee that we could previously depend on its signatures will no longer be, well, guaranteed.

My gut feeling tells me that will be a bad direction for us to take with PHP.  What do other's think?

================

After writing the above it occurred to me the solution to the above problem already conceptually exists, too, classes get to use the `abstract` keyword when their children need to be required to implement something.

If PHP requires interfaces with parameters defined as type `abstract` (or `never` if we must)  then those interfaces should also be required to be declared `abstract`:

     abstract interface CollectionInterface
     {
         public function add(abstract $input): self;
     }
     Class IntCollection implements CollectionInterface 
     {
         public function add(int $input): self;
     }

The primary tangible difference between abstract and concrete interfaces would be in documentation, reflection, and possible an is_abstract() function so that code that needs to ensure an exact specific interface could do so.

The benefit of this approach as it appears to me is that (concrete) interfaces can retain their same level of guarantee where abstract interfaces would not be required to maintain such a guarantee.  #jmtcw

Thoughts?

-Mike

---

## #115734 — ondrej@mirtes.cz — 2021-08-15T08:24:53+00:00

URL: https://externals.io/message/115712#115734 · In reply to: #115731

It’s true that having “never” in a parameter type (please use “parameter”
name for the method declaration, “argument” is what you pass to a parameter
when you call the method) would allow to use any type in an overriding
method from contravariance/LSP point of view. But at the same time the
interface with the “never” parameter type isn’t useful at all, because
“never” cannot accept any type.

Same as “never” in a return type means “this function never returns”,
“never” in a parameter type means “you can never call this”.

That’s because if you have interface “Foo" with method "doFoo(never $a):
void”, you might be able to override it with “class Bar implements Foo” and
method “doFoo(A $a)”, but at the same time it only allows you to call
“Bar::doFoo(new A())”, it doesn’t allow you to call “Foo::doFoo(new A())”.
Which is probably not what people expect from a polymorphic method. So
having the method on the interface “Foo” is completely useless.

Ondřej Mirtes

---

## #115735 — Jordan LeDoux — 2021-08-15T10:22:07+00:00

URL: https://externals.io/message/115712#115735 · In reply to: #115734

> It’s true that having “never” in a parameter type (please use “parameter”
> name for the method declaration, “argument” is what you pass to a parameter
> when you call the method) would allow to use any type in an overriding
> method from contravariance/LSP point of view. But at the same time the
> interface with the “never” parameter type isn’t useful at all, because
> “never” cannot accept any type.
>

I fixed the parameter/argument discrepancy in the wiki version of the RFC
and the pull request already. :)


>
> Same as “never” in a return type means “this function never returns”,
> “never” in a parameter type means “you can never call this”.
>
> That’s because if you have interface “Foo" with method "doFoo(never $a):
> void”, you might be able to override it with “class Bar implements Foo” and
> method “doFoo(A $a)”, but at the same time it only allows you to call
> “Bar::doFoo(new A())”, it doesn’t allow you to call “Foo::doFoo(new A())”.
> Which is probably not what people expect from a polymorphic method. So
> having the method on the interface “Foo” is completely useless.
>
> Ondřej Mirtes
>

I understand the point you are making, but my answer would be two simple
points:

1. Mixed is equally useless (in a different way). It's so much so, that
it's literally the same as not providing a type at all. Omitting types is
assumed to be mixed. While never is unhelpful in terms of static analysis,
mixed is unhelpful in terms of semantic correctness of the code itself. I
would argue that the issues around mixed are worse, because they create
problems in actual programs... never only creates problems for IDEs and
static analysis tools. And that's not me arguing that mixed should be
removed (not that it could be). My point is that the top type and the
bottom type in any type system are always going to have some inherent
limitations in one direction, while being inherently limitless in the other
direction... that's what makes them the top type or bottom type.

2. Interfaces have more purposes than just standing in for classes in a
type hint. Interfaces also guarantee an implementation. The ArrayAccess
interface is really the shining example of this RFC. It's parameters are
all typed as mixed, which prohibits any implementing class from typing at
all. This is strictly incorrect. The key cannot be, for instance, null or a
resource. Well... okay, I can think of some really terrifying ways you
*could* do that, but I think my point stands. The types for all the
parameters in the ArrayAccess interface should be never, because it is the
bottom type. What the ArrayAccess interface indicates is that the engine
can treat the object like an array for the purposes of the `[]` operators,
but lots of code which implements the interface has to do all kinds of type
checks that should be unnecessary due to the requirement of typing the
parameter as mixed.

So I guess what I'm saying is that all the limitations and drawbacks of
using this type that you mentioned are 100% true. But I don't think that's
a good reason to reject the feature personally. It would not be correct for
every interface to type all parameters as never if this were passed, the
same way that it wouldn't be correct for all interfaces to type all
parameters int. The never type does not fit all use cases, but for the use
cases it does fit, it helps avoid a lot of unnecessary code, it helps make
code more correct and safe, and it improves run time consistency.

This RFC is not about formalizing the never type. I think that's a good
follow up to this one, personally. That would involve formalizing the set
theory of never as the bottom type (which would involve some changes to
unions and intersections, as well as probably some other changes). For
instance, T|never should reduce to T if never is the bottom type, while
T&never should reduce to never. Similarly, T|mixed should reduce to mixed
(which it does), and T&mixed should reduce to T.

I suppose, I just don't really understand this argument. It's not the right
choice for every interface, but I didn't think it would be, and I hope I
didn't give the impression that I thought it would be. But that doesn't, to
me, affect the utility that the type *does* serve.

Jordan

---

## #115736 — Lynn — 2021-08-15T10:43:21+00:00

URL: https://externals.io/message/115712#115736 · In reply to: #115712

On Sat, Aug 14, 2021 at 1:27 AM Jordan LeDoux <jordan.ledoux@gmail.com>
wrote:

>
> Any feedback is greatly appreciated.
>

https://github.com/JordanRL/never-argument-type


My only feedback is that it should not be called `never`. If I see `never`
as the parameter type, I read this as never being allowed to pass anything,
which means that `foo('something')` with `foo(never $something)` will
result in a compile error. The type `never` only makes sense from a return
type perspective: "Should it return? never". Perhaps `any` or `anything`
makes more sense? I've also seen a suggestion to name it `abstract`, and
maybe there are more suggestions I've missed. To me it also sounds like the
goal of `never` as a parameter type is different from the return type. From
my understanding one indicates that a type is required, while the other
indicates that nothing ever returns, giving it the same name is confusing.


Apologies for sending this message twice to you Jordan, reply to all is not
my default reply button.

---

## #115737 — Deleu — 2021-08-15T11:05:00+00:00

URL: https://externals.io/message/115712#115737 · In reply to: #115736

> My only feedback is that it should not be called `never`. If I see `never`
> as the parameter type, I read this as never being allowed to pass anything,
> which means that `foo('something')` with `foo(never $something)` will
> result in a compile error. The type `never` only makes sense from a return
> type perspective: "Should it return? never". Perhaps `any` or `anything`
> makes more sense? I've also seen a suggestion to name it `abstract`, and
> maybe there are more suggestions I've missed. To me it also sounds like the
> goal of `never` as a parameter type is different from the return type. From
> my understanding one indicates that a type is required, while the other
> indicates that nothing ever returns, giving it the same name is confusing.
>

While I 100% agree that never is not a good name for the parameter type,
the reasoning is sound that never is now the bottom type in PHP since it
was introduced.

The fact that never as return type means it never returns, but as parameter
type means you can specify anything is just the effect of being LSP
complete. Since nothing can inherit never, then you can never return
something, but because any method can broaden their input capacity,
anything is broader than never.

This is where I think that for me the best option would be to have a sugar
aliasing for never provided by the engine. The potential aliases seem to be
any, anything, something or abstract. Making it an alias does mean that the
following would be allowed:


```
public function myMethod(never $weird): abstract;
```

But this is where coding style tools would come in and enforce that `never`
be only used for return type while it's alias (any, anything, something,
abstract) would only be used on parameter type.

I guess the huge benefit of using abstract as the type alias is that it's
already a reserved word. The more I think about it the more I like this RFC.

---

## #115738 — Jordan LeDoux — 2021-08-15T11:11:16+00:00

URL: https://externals.io/message/115712#115738 · In reply to: #115736

> On Sat, Aug 14, 2021 at 1:27 AM Jordan LeDoux <jordan.ledoux@gmail.com>
> wrote:
>
> > […]
>
> https://github.com/JordanRL/never-argument-type
>
>
> My only feedback is that it should not be called `never`. If I see `never`
> as the parameter type, I read this as never being allowed to pass anything,
> which means that `foo('something')` with `foo(never $something)` will
> result in a compile error. The type `never` only makes sense from a return
> type perspective: "Should it return? never". Perhaps `any` or `anything`
> makes more sense? I've also seen a suggestion to name it `abstract`, and
> maybe there are more suggestions I've missed. To me it also sounds like the
> goal of `never` as a parameter type is different from the return type. From
> my understanding one indicates that a type is required, while the other
> indicates that nothing ever returns, giving it the same name is confusing.
>
>
> Apologies for sending this message twice to you Jordan, reply to all is
> not my default reply button.
>

No worries :)

So your intuition is correct. `foo(never $something)` will result in a
`TypeError` if you try to call it directly. The only way it can be called
is by being overridden with a wider type, which is why most of the focus is
around inheritance. Technically, you *could* use it in a function and that
would make the function uncallable, because any function with a parameter
of type never is uncallable unless it is overridden and widened.

This is in line with type theory for how bottom types work in other
languages as well.

My biggest worry about using something besides `never` is that it *is* the
bottom type already. It's only usable in return types, but within the
engine it *is* the bottom type. That's what the bottom type in a type
system generally means: this can't be used and/or the program stops if this
is used. If we made a bottom type for returns (which we have), and then
made a *different* bottom type for arguments, we'd be in a situation that
is unique in programming languages as far as I can tell. I have done a bit
of research into this as part of the RFC, and I haven't found a single
language with multiple bottom types. :/

TypeScript, Elm, and PHP have the bottom type `never`. Ceylon, Kotlin, and
Scala have the bottom type `Nothing`. Rust has the bottom type `!`,
JavaScript with Closure Compiler annotations has `!Null` (which is intended
to mean a non-null member of the null type), Julia has `Union{}`, Python
has `typing.NoReturn`, and Lisp has `NIL`.

None of them (so far) have multiple bottom types for different
circumstances.

And the meaning of the type isn't *exactly* "any". It's more like "none".
But the type `string` contains the "none" concept also. You take "none" and
then you add the parts that make a string type to "none", and what you're
left with is just the string type. That's why it can be broadened to
anything, because *all* types contain `never`, just like you can subtract 0
from any integer.

I have been focusing on the use cases, which are about how this interacts
with widening and inheritance. But the type itself I would argue is fully
descriptive where it is defined: that code can never be called due to the
type on the parameter being never. It must be inherited. If we did
something like `any`, that would indicate that the intent for inheritance
more clearly, but think about the code it's actually defined in. If you had
the following code:

```
class A {
  public function doSomething(any $var): string {
  }
}
```

Would you expect calling that method to result in a `TypeError`? Because
that's what *should* happen. The code it is actually written in cannot
accept `any` type, in fact it can `never` accept a type.

Jordan

---

## #115739 — Lynn — 2021-08-15T11:40:36+00:00

URL: https://externals.io/message/115712#115739 · In reply to: #115738

On Sun, Aug 15, 2021 at 1:11 PM Jordan LeDoux <jordan.ledoux@gmail.com>
wrote:

>
> So your intuition is correct. `foo(never $something)` will result in a
> `TypeError` if you try to call it directly. The only way it can be called
> is by being overridden with a wider type, which is why most of the focus is
> around inheritance. Technically, you *could* use it in a function and that
> would make the function uncallable, because any function with a parameter
> of type never is uncallable unless it is overridden and widened.
>
> This is in line with type theory for how bottom types work in other
> languages as well.
>
> My biggest worry about using something besides `never` is that it *is* the
> bottom type already. It's only usable in return types, but within the
> engine it *is* the bottom type. That's what the bottom type in a type
> system generally means: this can't be used and/or the program stops if this
> is used. If we made a bottom type for returns (which we have), and then
> made a *different* bottom type for arguments, we'd be in a situation that
> is unique in programming languages as far as I can tell. I have done a bit
> of research into this as part of the RFC, and I haven't found a single
> language with multiple bottom types. :/
>
> TypeScript, Elm, and PHP have the bottom type `never`. Ceylon, Kotlin, and
> Scala have the bottom type `Nothing`. Rust has the bottom type `!`,
> JavaScript with Closure Compiler annotations has `!Null` (which is intended
> to mean a non-null member of the null type), Julia has `Union{}`, Python
> has `typing.NoReturn`, and Lisp has `NIL`.
>
> None of them (so far) have multiple bottom types for different
> circumstances.
>
> And the meaning of the type isn't *exactly* "any". It's more like "none".
> But the type `string` contains the "none" concept also. You take "none" and
> then you add the parts that make a string type to "none", and what you're
> left with is just the string type. That's why it can be broadened to
> anything, because *all* types contain `never`, just like you can subtract 0
> from any integer.
>
> I have been focusing on the use cases, which are about how this interacts
> with widening and inheritance. But the type itself I would argue is fully
> descriptive where it is defined: that code can never be called due to the
> type on the parameter being never. It must be inherited. If we did
> something like `any`, that would indicate that the intent for inheritance
> more clearly, but think about the code it's actually defined in. If you had
> the following code:
>
> ```
> class A {
>   public function doSomething(any $var): string {
>   }
> }
> ```
>
> Would you expect calling that method to result in a `TypeError`? Because
> that's what *should* happen. The code it is actually written in cannot
> accept `any` type, in fact it can `never` accept a type.
>
> Jordan
>
>
So the `never` just tells the developer "extend/implement me with a type",
that makes sense to me. Having multiple bottom types (and thus most likely
aliases) would probably make it even more confusing, you're right that this
is probably the best course of action 👍

---

## #115740 — drealecs@gmail.com — 2021-08-15T23:06:00+00:00

URL: https://externals.io/message/115712#115740 · In reply to: #115712

> Hey internals,
>
> I've been working on the draft for my operator overloading RFC, and in
> doing so I encountered a separate change that I would like to see.
>
> That is, the use of `never` as an argument type for interfaces. Since
> arguments in PHP are contravariant to preserve Liskov substitution, `never`
> as the bottom type should indicate that implementing classes can require
> any type combination they want. This is in fact consistent with type theory
> and set theory, and is how the bottom type is treated in several other
> languages.
>
> In this case, the bottom type would be used to indicate covariant parameter
> polymorphism while not conflicting with LSP.
>
> This would provide a sort of minimal form of generics to PHP without the
> issues that actual generics present from an implementation perspective. It
> would not, however, restrict or hinder any future RFC for generics.
>
> This is at the first draft stage, and I currently have the RFC on a github
> repo to allow for easy contribution and collaboration.
>
> Any feedback is greatly appreciated.
>

Hey Jordan,

From type perspective, this sounds good.

But, in my view, types should be checked statically.
For inheritance, when defining a method, both the parameters and return
types are nicely checked statically and that's good.
On the other side, when calling a method, due to the dynamic nature of PHP,
there is no simple way of checking it statically.

For example, if an interface is defining a method with a parameter of type
string and an implementation is changing the parameter type to a string|int
union, the caller that have a knowledge about the interface (that accepts
only string) can call it with an integer and everything will work fine.
It's kind of the same situation and this will too be flagged by the IDEs
and static analysis tools.
From a usage design perspective, this is bad, in my view. If caller knows
about the interface only, that is to ensure decoupling and this breaks it.

I was hoping that, at some point, we would be able to validate that
arguments types match parameters types statically, to avoid the continuous
runtime cost for it.
Maybe this can already be done, with some changes, when arguments are
properties defined with types.
When variables will have types and/or when variables will be
"final"/"constants", more static checks could be implemented.

Adding another exception to this behavior might not be the best idea if we
might move towards removing the behavior.

Coming back to the interface decoupling topic, that's exactly why
interfaces should be used, to allow polymorphism.
If I see an interface that was created just so it can be implemented by
multiple classes but the caller doesn't use the interface but it uses the
actual classes, I call it out as that is the wrong abstraction and the
interface should just be removed.

Your example with ArrayAccess is similar with this; when using it, you know
exactly what implementation you have. The interface is not helping here
related to polymorphism but it is actually required to allow a language
construct related to the engine itself, to translate accessing an object
with the same syntax as for an array.
If the 4 functions would have been magic methods, without an interface, you
would not have this problem.

Your example with CollectionInterface is exactly a place where I would say
the interface is unnecessary because you already know the implementation,
since you call it with the correct type. Just keeping the classes not
having to implement the interface would work just fine.

If you want to enforce method names on all Collection classes, you can do
that with other tools for static analysis, including code reviews; not with
the Interface.

Sorry for the long reply but my means to make it shorted are limited and
I'm using a smartphone only in this period (being in vacation).

Alex

---

## #115747 — Mike Schinkel — 2021-08-16T02:29:47+00:00

URL: https://externals.io/message/115712#115747 · In reply to: #115740

> 
> 
> If I see an interface that was created just so it can be implemented by
> multiple classes but the caller doesn't use the interface but it uses the
> actual classes, I call it out as that is the wrong abstraction and the
> interface should just be removed.
> 
> Your example with ArrayAccess is similar with this; when using it, you know
> exactly what implementation you have. The interface is not helping here
> related to polymorphism but it is actually required to allow a language
> construct related to the engine itself, to translate accessing an object
> with the same syntax as for an array.
> 
> If the 4 functions would have been magic methods, without an interface, you
> would not have this problem.

That is very insightful. 

It seems that back when PHP only had a hammer, PHP used a hammer. But now that PHP has a better tool for the job maybe PHP should change course?

More specifically, since ArrayAccess is primarily being used as a class annotation and not to enable polymorphism maybe the best course of action would be to deprecate having `ArrayAccess` the interface being an annotation and to instead having `#[ArrayAccess]` the attribute tell PHP to allow indexing instances of the class like an array?


> 
> 
> 2. Interfaces have more purposes than just standing in for classes in a
> type hint. Interfaces also guarantee an implementation.

Correct.  Which is why interfaces that allow for non-specific types — e.g. `mixed` or `never` — weaken that guarantee[1].

If `never`-parameter interfaces were to exist then code that passes a parameter to a `never`-parameter method would first have to check via reflection if the type if wants to pass will be accepted. That nullifies the guarantees of an interface.

I think the reason for envisioning `never` as an option to address the concerns it attempts to address is the unfortunately ramification of, as Alex identified, the choice to use an interface as a signal to provide language specific behavior to a class rather than to enforce polymorphic behavior. 

So rather than double down on features based on leaky abstractions better to backtrack and correct the original sin IMO.

-Mike

---

## #115748 — Jordan LeDoux — 2021-08-16T04:45:13+00:00

URL: https://externals.io/message/115712#115748 · In reply to: #115740

On Sun, Aug 15, 2021 at 4:06 PM Alexandru Pătrănescu <drealecs@gmail.com>
wrote:

> Hey Jordan,
>
> From type perspective, this sounds good.
>
> But, in my view, types should be checked statically.
> For inheritance, when defining a method, both the parameters and return
> types are nicely checked statically and that's good.
> On the other side, when calling a method, due to the dynamic nature of
> PHP, there is no simple way of checking it statically.
>
> For example, if an interface is defining a method with a parameter of type
> string and an implementation is changing the parameter type to a string|int
> union, the caller that have a knowledge about the interface (that accepts
> only string) can call it with an integer and everything will work fine.
> It's kind of the same situation and this will too be flagged by the IDEs
> and static analysis tools.
> From a usage design perspective, this is bad, in my view. If caller knows
> about the interface only, that is to ensure decoupling and this breaks it.
>
> I was hoping that, at some point, we would be able to validate that
> arguments types match parameters types statically, to avoid the continuous
> runtime cost for it.
> Maybe this can already be done, with some changes, when arguments are
> properties defined with types.
> When variables will have types and/or when variables will be
> "final"/"constants", more static checks could be implemented.
>

While that is something that I personally would not be opposed to working
towards, I don't see how that is possible so long as type juggling exists.
Unless PHP were to adopt a forced typing system similar to Java or C#,
there will always be cases where the engine is only able to resolve types
at runtime. Such an effort would doubtless result in a large reorganization
of how types interact with various parts of the language, so I don't see
that this would be more blocking towards such an effort than the many other
things currently in the language.

Any version of PHP that implemented such a plan would almost universally be
backwards incompatible with previous code. Not so much a major version
increment, but a parallel language syntax.

Perhaps I'm incorrect on that account. I admit to having much less
knowledge of that particular area of the engine than others on this list.


> Adding another exception to this behavior might not be the best idea if we
> might move towards removing the behavior.
>
> Coming back to the interface decoupling topic, that's exactly why
> interfaces should be used, to allow polymorphism.
> If I see an interface that was created just so it can be implemented by
> multiple classes but the caller doesn't use the interface but it uses the
> actual classes, I call it out as that is the wrong abstraction and the
> interface should just be removed.
>
> Your example with ArrayAccess is similar with this; when using it, you
> know exactly what implementation you have. The interface is not helping
> here related to polymorphism but it is actually required to allow a
> language construct related to the engine itself, to translate accessing an
> object with the same syntax as for an array.
> If the 4 functions would have been magic methods, without an interface,
> you would not have this problem.
>
> Your example with CollectionInterface is exactly a place where I would say
> the interface is unnecessary because you already know the implementation,
> since you call it with the correct type. Just keeping the classes not
> having to implement the interface would work just fine.
>
> If you want to enforce method names on all Collection classes, you can do
> that with other tools for static analysis, including code reviews; not with
> the Interface.
>
> Sorry for the long reply but my means to make it shorted are limited and
> I'm using a smartphone only in this period (being in vacation).
>
> Alex
>

I think this gets towards a fundamental idea that is much more sweeping,
that Mike also briefly touched on: interfaces are used currently to control
access to certain language features for objects. This helps preserve these
features through inheritance, but also represents problems like what you
are describing.

This RFC is fairly simple and straightforward. The entire patch is only 7
lines of code in two files, with the entire rest of the patch being tests.
I felt this would be a more acceptable way to approach the change as it is
consistent with type theory and the type system (as you noted), while being
the minimal change to enable the behavior intended.

The points that have been raised by you and others, including Nikic and
Ondrej, are absolutely true, as I've agreed before. There are limitations
and contradictions in using Interfaces in this way. However, it is also
consistent with currently implemented and maintained features. Some of
these, such as ArrayAccess, are currently implemented in what is
fundamentally a broken way. This RFC would not make that implementation
"unbroken", but would move the broken behavior from the runtime to the
static analysis tools.

The only real way to fix this fully, that I can see, is to not use
interfaces at all for controlling engine feature access for objects. If we
were to go that route, not only would this particular effort be much
larger, but there would be significant BC breaks. That is... perhaps
acceptable, but would certainly be more contentious. Since this was not
part of my research or the original design work I did for the RFC, please
understand that this is a spur-of-the-moment and arbitrary example, but it
would probably result in something like:

```
class A has ArrayLike {
}
```

Instead of:

```
class A implements ArrayAccess {
}
```

There would be downsides to this as well, as such features *do* require an
implementation of some kind of hook, and that hook *will* have different
typing requirements for different implementations. If Interfaces are not
the correct way to handle such hooks, and we feel it is worth the BC break
and the effort to "fix" existing implementations, we would still be left
with an inheritable quality that can't be directly typed against in static
analysis.

In reality, what I believe static analysis tools and IDEs would have to do
to resolve this is to assume that any internal structure which calls for a
`never` type or whatever equivalent we choose will accept a `mixed` type
argument, as it can be arbitrarily widened and must be widened in order to
be called successfully.

It appears that the list is in agreement that this RFC is a good
implementation of the type theory behind the change, but is interested in
discussing the idea of addressing a much, much larger issue that this
brings to the forefront. Is this an accurate summary Alex, Nikic, and
Ondrej?

Jordan

---

## #115752 — Nikita Popov — 2021-08-16T07:51:17+00:00

URL: https://externals.io/message/115712#115752 · In reply to: #115720

On Sat, Aug 14, 2021 at 3:44 PM Jordan LeDoux <jordan.ledoux@gmail.com>
wrote:

>
>
> 
>
> > […]
> When used as a sort of... pseudo-generics replacement, you'd need to use
> Docblocks to specify these, because **this feature is not generics** (which
> you correctly pointed out). I probably should have made that MORE clear so
> as to not confuse or trick anyone.
>
> If this RFC were passed, it could be sort of used like generics but it
> would be a bit hacky to use it that way as your example illustrates. In the
> absence of generics, this would probably be used as a stopgap in
> combination with docblocks. That's the point I was trying to make. :)
>
> The main value I see from an inheritance perspective is using never to
> disallow an omitted type. The inheriting class may specify *any* type, even
> mixed, but it must do so explicitly.
>

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.

The only case where this is somewhat sensible is with interfaces
controlling engine behavior, like the operator overloading interfaces
Addable etc you clearly have in mind here. If you never actually type
against them and only use them as a marker for the engine, then this works
out fine. But this still leaves the interface useless from a typesystem
perspective, it merely becomes an engine marker.

We have a better way to specify markers for the engine: Magic methods. I
think some people have a mistaken notion that engine-integrated interfaces
are always better than magic methods. This is only the case if such
interfaces are actually useful from a type system perspective. For example,
Countable and Traversable are useful magic interfaces, because you can
sensibly type against them. The recently introduced Stringable interface
(for the magic method __toString) falls in the same category.

Conversely, Serializable is actively harmful as a magic interface (apart
from the other issues with it), because whether an class implements
Serializable does not determine whether it is serializable -- all objects
are a priori serializable, the Serializable interface just gives it custom
serialization behavior. You'll note that the new __serialize/__unserialize
methods are plain magic methods without an interface. With exception of a
custom serializer implementation, user code should never be checking for
Serializable.

The operator overloading case is in between: The interface is not actively
harmful, but they also aren't useful. Given the lack of generics, it's not
really possible to write code against a "Multiplyable" interface that
actually provides a useful guarantee. The interface does not distinguish
whether "T * T" is valid, or only scalar multiplication "T * float" is
supported. When working with operator overloads in PHP, I expect usage to
type against a specific class implementing operator overloading, say Money,
rather than typing against something like Addable&Multiplyable. The latter
would accept both Money and Matrix, both of which have entirely different
rules on the kinds of operands they accept, even if they are, in some
sense, addable and multiplyable.

Regards,
Nikita

---

Next page: https://externals.io/message/115712.md?page=2
