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