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. -- PHP Internals - PHP Runtime Development Mailing List To unsubscribe, visit: http://www.php.net/unsub.php