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
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
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
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
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
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
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
--
To unsubscribe, visit: https://www.php.net/unsub.php
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.
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.
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
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
On Sat, Aug 14, 2021 at 3:44 PM Jordan LeDoux jordan.ledoux@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
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
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
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.
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
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:
- 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.) - 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"? - 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
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
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
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
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:
-
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. -
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
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
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:
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
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 meaninglessmixed
... 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 implementArrayAccess
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
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.
My only feedback is that it should not be called
never
. If I seenever
as the parameter type, I read this as never being allowed to pass anything,
which means thatfoo('something')
withfoo(never $something)
will
result in a compile error. The typenever
only makes sense from a return
type perspective: "Should it return? never". Perhapsany
oranything
makes more sense? I've also seen a suggestion to name itabstract
, and
maybe there are more suggestions I've missed. To me it also sounds like the
goal ofnever
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.
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 seenever
as the parameter type, I read this as never being allowed to pass anything,
which means thatfoo('something')
withfoo(never $something)
will
result in a compile error. The typenever
only makes sense from a return
type perspective: "Should it return? never". Perhapsany
oranything
makes more sense? I've also seen a suggestion to name itabstract
, and
maybe there are more suggestions I've missed. To me it also sounds like the
goal ofnever
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
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 typeNothing
. 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 hasUnion{}
, Python
hastyping.NoReturn
, and Lisp hasNIL
.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 typestring
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 containnever
, 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 likeany
, 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
acceptany
type, in fact it cannever
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 👍
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
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?
- 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
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
Hello list,
to revive this old RFC.
One really nice application of never parameters is with intersection types:
interface I {}
interface ReturnI {
public function foo(never $x): I;
}
interface AcceptI {
public function foo(I $x): mixed;
}
function f(ReturnI&AcceptI $arg, I $x): I {
return $arg->foo($x);
}
Look at a live example where I simulate the "never" with an
"AlmostNever" interface.
https://3v4l.org/dPbgt
It can even be applied with additional parameters:
https://3v4l.org/l8eG6
An application can use this to reduce the range of individual
interfaces, and cover scenarios that would otherwise require generics
or callable types.
So.. I would really like to see it :)
--- Andreas
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