This discussion made me have another look at the Generics RFC,
https://wiki.php.net/rfc/generics

It seems to me that the proposal violates LSP, because it does not
correctly implement contravariance.
Look at the part where it talks about instanceof.

interface Feline {}
class Cat implements Feline {}
class Tiger implements Feline {}

class Box<T is Feline> {
  function entrap(T $feline) {}
}

$feline_box = new Box<Feline>();
$cat_box = new Box<Cat>();
$tiger_box = new Box<Tiger>();

$cat = new Cat();
$tiger = new Tiger();

assert($feline_box instanceof Box<Feline>);  // -> ok.
assert($tiger_box instanceof Box<Feline>);  // -> ok.
assert($cat_box instanceof Box<Feline>);  // -> ok.

assert($cat instanceof Feline);  // -> ok.
assert($tiger instanceof Feline);  // -> ok.

$feline_box->entrap($cat);  // -> ok.
$cat_box->entrap($cat); // -> ok.
$tiger_box->entrap($cat); // -> Fatal error: Uncaught TypeError.


So, even with generics, we still need to think about contravariance.

We need to distinguish 3 types of type parameter on classes:
1. Those which are used in method return types.
2. Those which are used in method parameter types.
3. Those which are used in both.

For these 3 cases, the following rules would need to apply:
1. Contravariance.
2. Covariance
3. Identity.

E.g.

interface Fruit;
interface Banana extends Fruit;

interface Grower<T is Fruit> {
  function grow() : T;
}

interface Processor<T is Fruit> {
  function process(T $fruit) : T;
}

interface Eater<T is Fruit> {
  function eat(T $fruit);
}

// Covariance
var_dump(new Grower<Banana> instanceof Grower<Fruit>); // => (bool) true
var_dump(new Grower<Fruit> instanceof Grower<Banana>); // => (bool) false

// Identity
var_dump(new Processor<Banana> instanceof Processor<Fruit>); // => (bool) false
var_dump(new Processor<Fruit> instanceof Processor<Banana>); // => (bool) false

// Contravariance
var_dump(new Eater<Banana> instanceof Eater<Fruit>); // => (bool) false
var_dump(new Eater<Fruit> instanceof Eater<Banana>); // => (bool) true


The only supertype for all Eater<*> types would be Eater<EMPTY_TYPE>.
This super-eater has the absolute fruit allergy.




On Wed, Aug 23, 2017 at 9:18 AM, Michał Brzuchalski
<mic...@brzuchalski.com> wrote:
> Hi Andreas,
>
> 2017-08-22 21:11 GMT+02:00 Andreas Hennings <andr...@dqxtech.net>:
>
>> On Tue, Aug 22, 2017 at 10:39 AM, Nikita Popov <nikita....@gmail.com>
>> wrote:
>> > On Tue, Aug 22, 2017 at 4:27 AM, Andreas Hennings <andr...@dqxtech.net>
>> > wrote:
>> >>
>> >> Hello list,
>> >> for a while I had this thought about contravariance and an "empty type".
>> >> I don't expect too much of it, for now I just want to share the idea.
>> >> Maybe this concept even exists somewhere in a different language, and
>> >> I am not aware of it.
>> >>
>> >> I think it has some overlap with generics,
>> >> https://wiki.php.net/rfc/generics.
>> >>
>> >> ------------
>> >>
>> >> I think I am not the first one to suggest allowing contravariance for
>> >> method parameters.
>> >> E.g. here, "PHP RFC: Parameter Type Widening"
>> >> https://wiki.php.net/rfc/parameter-no-type-variance
>> >>
>> >> From this RFC:
>> >> > Unfortunately “true” contravariance for class types isn't part of this
>> >> > RFC, as implementing that is far more difficult, and would require
>> >> > additional rules about autoloading and/or class compilation, which
>> might
>> >> > only be acceptable at a major release.
>> >>
>> >> For anyone not familiar with the term:
>> >>
>> >> interface I {
>> >>   function foo(J $arg);
>> >> }
>> >>
>> >> interface J extends I {
>> >>   function foo(I $arg);
>> >> }
>> >>
>> >> So: While return types in a child method should be either the same or
>> >> more narrow, the parameter types should be either the same or more
>> >> permissive.
>> >> Without this it would break Liskov substitution.
>> >>
>> >> ---------------
>> >>
>> >> Now for my actual proposal: The "empty type".
>> >> We can think of a type (class/interface or primitive) as a set or a
>> >> constraint on the kind of values that it allows.
>> >> There is a special type, "mixed", which allows all values. We could
>> >> also think of it as the union of all types.
>> >>
>> >> A natural extension of this concept, on the other end, would be a type
>> >> "nothing" or "empty", which would allow no values at all.
>> >> We could think of this as the intersection of all types.
>> >> In fact it is already sufficient to intersect just two distinct
>> >> primitive types to get this empty type:
>> >> "All values that are at the same time string and integer" clearly is
>> >> an empty type.
>> >>
>> >> How would this ever be useful?
>> >> If we write a base class or interface for a category of interfaces
>> >> that have a similar signature.
>> >>
>> >> 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);
>> >> }
>> >>
>> >> One could imagine a component that has a list of AbstractFruitEater
>> >> objects, and chooses one that is suitable for the given fruit, using
>> >> instanceof.
>> >> I think the correct term is "chain of responsibility".
>> >>
>> >> function eatApple(array $fruitEaters, Apple $apple) {
>> >>   foreach ($fruitEaters as $eater) {
>> >>     if ($eater instanceof AppleEater) {
>> >>       $eater->eat($apple);
>> >>       break;
>> >>     }
>> >>   }
>> >> }
>> >>
>> >> --------------------
>> >>
>> >> We can go one step further.
>> >>
>> >> The natural parameter type to use for param $fruit in
>> >> AbstractFruitEater::foo() would not be the global EMPTY_TYPE, but
>> >> something more specific:
>> >> The projected intersection of all real and hypothetical children of
>> >> interface Fruit.
>> >> Obviously this does not and cannot exist as a class or interface.
>> >>
>> >> Practically, for the values it allows, this is the same as the global
>> >> EMPTY_TYPE.
>> >> But unlike the EMPTY_TYPE, this would poses a restriction on the
>> >> parameter type in child interfaces.
>> >>
>> >> What would be the syntax / notation for such a projected hypothetical
>> >> subtype?
>> >> I don't know. Let's say INTERSECT_CHILDREN<Fruit>
>> >>
>> >> So, would the following work?
>> >>
>> >> interface Food {..}
>> >> interface Fruit extends Food {..}
>> >> interface Banana extends Fruit {..}
>> >>
>> >> interface AbstractFoodEater {
>> >>   function eat(INTERSECT_CHILDREN<Food> $food);
>> >> }
>> >>
>> >> interface AbstractFruitEater extends AbstractFoodEater {
>> >>   function eat(INTERSECT_CHILDREN<Fruit> $fruit);
>> >> }
>> >>
>> >> interface BananaEater extends AbstractFruitEater {
>> >>   function eat(Banana $banana);
>> >> }
>> >>
>> >> I'm not sure.
>> >> Liskov would not care. Both AbstractFoodEater and AbstractFruitEater
>> >> are useless on their own.
>> >> Maybe there are other logical conflicts which I don't see.
>> >>
>> >>
>> >> ----------
>> >>
>> >> Obviously with generics this base interface would no longer be relevant.
>> >> https://wiki.php.net/rfc/generics
>> >>
>> >> interface FruitEater<FruitType> {
>> >>   function eat(FruitType $fruit);
>> >> }
>> >>
>> >> // This is not really necessary.
>> >> interface BananaEater extends FruitEater<Banana> {
>> >>   function eat(Banana $banana);
>> >> }
>> >>
>> >> So, would the "empty type" become obsolete? Maybe.
>> >> I did not arrive at a final conclusion yet. It still seems too
>> >> interesting to let it go.
>> >>
>> >> -- Andreas
>> >
>> >
>> > What's the purpose of this construction? I get the general idea (work
>> around
>> > LSP variance restrictions without generics), but I don't see how the
>> > practical use would look like.
>>
>> To be honest I am still not fully convinced myself.
>> I just couldn't resist because this idea was haunting me for too long.
>>
>> > After all, using the empty type as an
>> > argument implies that the method may not ever be called, so wouldn't an
>> > interface using it be essentially useless?
>> >
>> > Nikita
>>
>> Interfaces like AbstractFruitEater would mainly be used to categorize
>> its child interfaces, and as a formalized constraint on method
>> ::eat().
>>
>> Any child interface of AbstractFruitEater must have a method eat(),
>> which must have exactly one required parameter (and as many optional
>> parameters as it wants). This parameter must have a type hint
>> compatible with the constraint mentioned above (in case of EMPTY_TYPE,
>> there is no constraint on the parameter type, it could as well be
>> "mixed").
>>
>> Any component that wants to call $eater->eat($apple) on an $eater of
>> type AbstractFruitEater, needs to do one of two things first:
>> - Use reflection to check for the first parameter's type, if it allows
>> Apple.
>> - Use instanceof to check if it implements AppleEater.
>>
>> If the $eater was only type-hinted as "object" instead of
>> AbstractFruitEater, the reflection would have to do more work. It
>> would have to check if a method eat() exists, and then check the first
>> parameter's type.
>>
>> A component I might have built with the EMPTY_TYPE or with
>> INTERSECT_CHILDREN<Food> would be something like this:
>>
>>
>> // Base interface for eaters that only eat a specific fruit type.
>> interface AbstractSpecificFruitEater {
>>   function eat(INTERSECT_CHILDREN<Food> $fruit);
>> }
>>
>> // Interface for eaters that eat any fruit.
>> // This could extend AbstractSpecificFruitEater, but doesn't have to.
>> interface FruitEater /* extends AbstractSpecificFruitEater */ {
>>   function eat(Fruit $fruit);
>> }
>>
>> class ChainedFruitEater implements FruitEater {
>>   private $eaters = [];
>>   public function addSpecificEater(AbstractSpecificFruitEater $eater) {
>>     $paramClass = (new
>> \ReflectionObject($eater))->getMethod('eat')->
>> getParameters()[0]->getClass();
>>     $this->eaters[$paramClass] = $eater;
>>   }
>>   public function eat(Fruit $fruit) {
>>     if (null !== $specificEater = $this->findSuitableEater($fruit)) {
>>       $specificEater->eat($fruit);
>>       return true;
>>     }
>>     else {
>>       return false;
>>     }
>>   }
>>   private function findSuitableEater(Fruit $fruit) {
>>     foreach ($this->eaters as $paramClass => $eater) {
>>       if ($fruit instanceof $paramClass) {
>>         return $eater;
>>       }
>>     }
>>   }
>> }
>>
>>
>> Without the EMPTY_TYPE or INTERSECT_CHILDREN<Food>, the interface
>> AbstractSpecificFruitEater could not define a method ::eat().
>>
>> Classes implementing AbstractSpecificFruitEater would not know that a
>> method ::eat() is required, and what structure it must have.
>>
>> The reflection line would need to check if the method exists, if the
>> method is public and non-static, if the parameter exists, if it has a
>> type hint class.
>>
>>
>> In the end I implemented this another way.
>> My specific fruit eaters now always accept any fruit, but do an
>> instanceof check inside. They have an added method like
>> "acceptsFruitClass($class)".
>>
>> I don't know if I would replace my current implementation with the code
>> above.
>> I think I rather wait for generics.
>>
>>
>> NOTE: When I say "type hint", I do not distinguish what is currently
>> implemented natively, what is in the @param PhpDoc, and what might be
>> implemented natively in the future. E.g. I don't even know if "mixed"
>> or "object" is currently implemented or not in latest PHP 7.
>>
>>
> "object" type hint and return type is a part of current 7.2 release,
> "mixed" not
>
>
>> --
>> PHP Internals - PHP Runtime Development Mailing List
>> To unsubscribe, visit: http://www.php.net/unsub.php
>>
>>
>
>
> --
> regards / pozdrawiam,
> --
> Michał Brzuchalski
> about.me/brzuchal
> brzuchalski.com

--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php

Reply via email to