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

Reply via email to