[RFC] Never For Argument Types

  115712
August 13, 2021 23:27 jordan.ledoux@gmail.com (Jordan LeDoux)
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
August 14, 2021 08:39 deleugyn@gmail.com (Deleu)
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.

On Sat, Aug 14, 2021, 01:27 Jordan LeDoux 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 > > Jordan >
  115715
August 14, 2021 12:48 george.banyard@gmail.com ("G. P. B.")
On Sat, 14 Aug 2021 at 10:55, Deleu <deleugyn@gmail.com> wrote:

> 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
August 14, 2021 13:04 larry@garfieldtech.com ("Larry Garfield")
On Sat, Aug 14, 2021, at 7:48 AM, G. P. B. wrote:
> On Sat, 14 Aug 2021 at 10:55, Deleu <deleugyn@gmail.com> wrote: > > > 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
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
  116022
September 9, 2021 03:44 andreas@dqxtech.net (Andreas Hennings)
On Sat, 14 Aug 2021 at 15:05, Larry Garfield <larry@garfieldtech.com> wrote:
> > On Sat, Aug 14, 2021, at 7:48 AM, G. P. B. wrote: > > On Sat, 14 Aug 2021 at 10:55, Deleu <deleugyn@gmail.com> wrote: > > > > > 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 > > 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.
You don't have to specify a type. When overriding the method in a child interface or abstract class, it must have a parameter in the same place (and ideally with same name, to support named argument calls). The parameter in the overridden method may or may not have a type hint. The parameter type hint can be "never", as in the parent, but then you cannot call the method. See also this older discussion, https://externals.io/message/100275#100300 Example: interface Fruit {..} interface Apple extends Fruit {..} interface Banana extends Fruit {..} interface AbstractFruitEater { function eat(EMPTY_TYPE $fruit); } interface BananaEater extends AbstractFoodEater { function eat(Banana $banana); } interface AppleEater extends AbstractFoodEater { function eat(Apple $apple); } // In an ideal world, UniversalFruitEater would extend every other FruitEater type, but that's not really possible. interface UniversalFruitEater extends AbstractFoodEater /* , BananaEater, AppleEater */ { function eat(Fruit $fruit); } ---- Btw, I wonder what this means for optional parameters. Currently, "null" is not allowed as a parameter type hint, but it would be a natural bottom type for "?*". How narrow-minded of PHP! E.g. interface AbstractOptionalFruitEater { function eat(null $fruit); // Not allowed, but would make sense here. } interface OptionalBananaEater extends AbstractFoodEater { function eat(?Banana $banana); } interface OptionalAppleEater extends AbstractFoodEater { function eat(?Apple $apple); } ---------- I am not sure yet how useful this really is in practice, if we cannot "close the circle" and have UniversalFruitEater extend all the other FruitEater interfaces, and if we don't have generics. The first idea would be a mapper that chooses a suitable eater like so: abstract class MapperEaterBase implements UniversalFruitEater { public function eat(Fruit $fruit) { // IDE will get confused here, because it cannot verify that the eater from findSuitableEater() is actually suitable. return $this->findSuitableEater($fruit)->eat($fruit); } abstract protected function findSuitableEater(Fruit $fruit): AbstractFruitEater; } This would be a kind of userland "method overloading". The "map of eaters" could be built with reflection, or with repeated try/catch until one of them works. However, I am still undecided whether this is the best way to achieve this. I am currently building a system similar to this, but here every implementation has a method that reports which types are supported. My equivalent to the eat() method simply accepts anything, and then uses instanceof internally to reject parameters with the wrong type. The "never" type for parameters could provide some new possibilities here. -- Andreas
> > --Larry Garfield > > -- > PHP Internals - PHP Runtime Development Mailing List > To unsubscribe, visit: https://www.php.net/unsub.php >
  115717
August 14, 2021 13:12 deleugyn@gmail.com (Deleu)
On Sat, Aug 14, 2021, 14:48 G. P. B. banyard@gmail.com> wrote:

> On Sat, 14 Aug 2021 at 10:55, Deleu <deleugyn@gmail.com> wrote: > >> 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. >
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
August 14, 2021 13:25 nikita.ppv@gmail.com (Nikita Popov)
On Sat, Aug 14, 2021 at 1:27 AM Jordan LeDoux 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 { public function add(T $input): self; } and the use changes to function addMultiple(CollectionInterface $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
August 14, 2021 13:43 jordan.ledoux@gmail.com (Jordan LeDoux)
On Sat, Aug 14, 2021 at 6:25 AM Nikita Popov ppv@gmail.com> wrote:

> > 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
  115752
August 16, 2021 07:51 nikita.ppv@gmail.com (Nikita Popov)
On Sat, Aug 14, 2021 at 3:44 PM Jordan LeDoux ledoux@gmail.com>
wrote:

> > > On Sat, Aug 14, 2021 at 6:25 AM Nikita Popov ppv@gmail.com> wrote: > >> >> 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. >
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
  115754
August 16, 2021 08:11 faizanakram99@gmail.com (Faizan Akram Dar)
On Aug 16 2021, at 1:21 pm, Nikita Popov ppv@gmail.com> wrote:
> On Sat, Aug 14, 2021 at 3:44 PM Jordan LeDoux ledoux@gmail.com> > wrote: > > > > > > > On Sat, Aug 14, 2021 at 6:25 AM Nikita Popov ppv@gmail.com> wrote: > > > >> > >> 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. > > > > 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 >
Hi, I think you make a very good point here that it only helps as a marker in engine-integrated interfaces. That alone will allow usage of "specific types" (in parameters) when implementing them. Also, as per OP, the patch is really trivial and might also help in operator overloading (in future).
> The operator overloading case is in between: The interface is not actively > harmful, but they also aren't useful. >
I think they are still useful in a code like this. public function price(Quantity $quantity, Amount $amount, \Addable ...$additionalCosts) { $otherCosts = array_reduce( $additionalCosts, static fn (int $total, \Addable $cost) => $cost + $total, ); return ($amount / $quantity) + $otherCosts; } Regards, Faizan
  115759
August 16, 2021 14:59 jordan.ledoux@gmail.com (Jordan LeDoux)
On Mon, Aug 16, 2021 at 12:51 AM Nikita Popov ppv@gmail.com> wrote:

> 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. > > What would be your thoughts for the case of ArrayAccess? The engine
integration can't really be used without multiple methods being implemented, so some form of contractual implementation does make sense. Additionally, the interface does provide some useful typing features, such as ArrayAccess|array. However the interface itself doesn't guarantee much about the actual implementation while being a prime candidate for something like the never type proposed here. In the case of ArrayAccess, it sort of exists to provide operator overloading for []. But I don't think it'll be the last time that we encounter an engine feature that is best provided through multiple method implementations.
> 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 >
To be honest, I don't imagine there are many cases where the __add() method would use the Addable interface (assuming it existed) as a way to type the second operand. That would only guarantee that the *other* operand could handle the adding if it had to, but doesn't tell the called method anything about how to access the value that the object represents which needs to be added. I think it is far more likely that it'll be typed against specific classes, like you're suggesting, in almost all uses of the feature. If for no other reason than simply the fact that different values mean different things to different objects. Perhaps it would lead to a community standard around types of values that *should* be interoperable, something like a Numberlike interface, but I have purposely avoided venturing into that so far in my draft of that RFC because it seemed too opinionated to me, and I wanted to avoid getting into the domain-specific implementations that some were suggesting. That could easily spiral into a multitude of very specific implementations ending up in core, when they more naturally belong in the context aware user space of the application they are used in. I am open to suggestions on alternatives. I will also be thinking about a form this might take that would address this concern while still providing a cleaner way for engine-like object shapes to be typed. I feel like this is one of the less disruptive ways that could be done, but less disruptive obviously means that it will inherit any lingering issues that may exist in that area. Jordan
  115769
August 22, 2021 04:15 jordan.ledoux@gmail.com (Jordan LeDoux)
Hey all,

Thanks for the feedback and the engagement on this RFC. I'll be withdrawing
this RFC as it's clear that its drawbacks are too large for the benefits it
does provide from internals' perspective. Essentially, I saw a very small
and safe change that could be made to the engine that would instantly
provide covariant parametric polymorphism, and I wanted to explore if it
was an option. Instead, I'll probably take the feedback on this RFC and
roll it into a much more comprehensive approach to that particular feature,
perhaps centered around something like abstract interfaces or templates.

The engine-hook scenarios that this particular RFC would serve have other
ways of being addressed, one of which I utilize in the operator overload
RFC by forcing explicit typing and then providing no interfaces at all. I
do not know if I'll try and approach the more comprehensive approach to LSP
preserving covariant parametric polymorphism for 8.2, but it'll be
something I work on here and there. If anyone else wants to pick that up
sooner contact me directly and I'll hand over my research and work to date.

In any case, thank you all for the feedback!

Jordan
  115721
August 14, 2021 14:01 php.lists@allenjb.me.uk (AllenJB)
On 14/08/2021 00:27, Jordan LeDoux 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 > > 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
August 14, 2021 14:39 george.banyard@gmail.com ("G. P. B.")
On Sat, 14 Aug 2021 at 16:01, AllenJB lists@allenjb.me.uk> wrote:

> 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
  115726
August 14, 2021 15:41 jordan.ledoux@gmail.com (Jordan LeDoux)
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
  115724
August 14, 2021 14:40 jordan.ledoux@gmail.com (Jordan LeDoux)
On Sat, Aug 14, 2021 at 7:01 AM AllenJB lists@allenjb.me.uk> wrote:

> > 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
  115731
August 15, 2021 01:58 mike@newclarity.net (Mike Schinkel)
> On Aug 14, 2021, at 10:40 AM, Jordan LeDoux ledoux@gmail.com> wrote: > > 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
August 15, 2021 08:24 ondrej@mirtes.cz (=?UTF-8?Q?Ond=C5=99ej_Mirtes?=)
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
August 15, 2021 10:22 jordan.ledoux@gmail.com (Jordan LeDoux)
On Sun, Aug 15, 2021 at 1:25 AM Ondřej Mirtes <ondrej@mirtes.cz> wrote:

> 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
  115728
August 14, 2021 16:19 matthewmatthew@gmail.com (Matthew Brown)
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

On Fri, 13 Aug 2021 at 19:27, Jordan LeDoux 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 > > Jordan >
  115729
August 14, 2021 17:12 claude.pache@gmail.com (Claude Pache)
> > On Fri, 13 Aug 2021 at 19:27, Jordan LeDoux 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 >> >> Jordan >>
> 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
August 14, 2021 17:21 jordan.ledoux@gmail.com (Jordan LeDoux)
On Sat, Aug 14, 2021 at 10:12 AM Claude Pache 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
  115736
August 15, 2021 10:43 kjarli@gmail.com (Lynn)
On Sat, Aug 14, 2021 at 1:27 AM Jordan LeDoux 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
August 15, 2021 11:05 deleugyn@gmail.com (Deleu)
On Sun, Aug 15, 2021, 12:43 Lynn <kjarli@gmail.com> wrote:

> 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
August 15, 2021 11:11 jordan.ledoux@gmail.com (Jordan LeDoux)
On Sun, Aug 15, 2021 at 3:43 AM Lynn <kjarli@gmail.com> wrote:

> On Sat, Aug 14, 2021 at 1:27 AM Jordan LeDoux 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. >
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
August 15, 2021 11:40 kjarli@gmail.com (Lynn)
On Sun, Aug 15, 2021 at 1:11 PM Jordan LeDoux 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
August 15, 2021 23:06 drealecs@gmail.com (=?UTF-8?Q?Alexandru_P=C4=83tr=C4=83nescu?=)
On Sat, Aug 14, 2021, 01:27 Jordan LeDoux 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. >
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
August 16, 2021 02:29 mike@newclarity.net (Mike Schinkel)
> On Aug 15, 2021, at 7:06 PM, Alexandru Pătrănescu <drealecs@gmail.com> wrote: > > 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?
> On Aug 15, 2021, at 6:22 AM, Jordan LeDoux ledoux@gmail.com> wrote: > > 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
August 16, 2021 04:45 jordan.ledoux@gmail.com (Jordan LeDoux)
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