Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:100288 Return-Path: Mailing-List: contact internals-help@lists.php.net; run by ezmlm Delivered-To: mailing list internals@lists.php.net Received: (qmail 79903 invoked from network); 23 Aug 2017 07:18:27 -0000 Received: from unknown (HELO lists.php.net) (127.0.0.1) by localhost with SMTP; 23 Aug 2017 07:18:27 -0000 Authentication-Results: pb1.pair.com smtp.mail=michal@brzuchalski.com; spf=pass; sender-id=pass Authentication-Results: pb1.pair.com header.from=michal@brzuchalski.com; sender-id=pass Received-SPF: pass (pb1.pair.com: domain brzuchalski.com designates 188.165.245.118 as permitted sender) X-PHP-List-Original-Sender: michal@brzuchalski.com X-Host-Fingerprint: 188.165.245.118 ns220893.ip-188-165-245.eu Received: from [188.165.245.118] ([188.165.245.118:46162] helo=poczta.brzuchalski.com) by pb1.pair.com (ecelerity 2.1.1.9-wez r(12769M)) with ESMTP id BF/EB-34801-B3C2D995 for ; Wed, 23 Aug 2017 03:18:22 -0400 Received: from localhost (localhost.localdomain [127.0.0.1]) by poczta.brzuchalski.com (Postfix) with ESMTP id 9197C2984276 for ; Wed, 23 Aug 2017 09:18:15 +0200 (CEST) Received: from poczta.brzuchalski.com ([127.0.0.1]) by localhost (poczta.brzuchalski.com [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id 4xtvgWW7WWNG for ; Wed, 23 Aug 2017 09:18:13 +0200 (CEST) Received: from mail-wm0-f47.google.com (unknown [74.125.82.47]) by poczta.brzuchalski.com (Postfix) with ESMTPSA id 721DA29842CA for ; Wed, 23 Aug 2017 09:18:02 +0200 (CEST) Received: by mail-wm0-f47.google.com with SMTP id z132so8976439wmg.1 for ; Wed, 23 Aug 2017 00:18:02 -0700 (PDT) X-Gm-Message-State: AHYfb5iPNFl8K3jGFTCij/zj7xH89BaUl6jYnbaKSAtRT66SEn1m8iMa DSilmUkOU7mEtDCbOMEby4XoewT9rw== X-Received: by 10.28.8.4 with SMTP id 4mr1493248wmi.123.1503472682132; Wed, 23 Aug 2017 00:18:02 -0700 (PDT) MIME-Version: 1.0 Received: by 10.223.142.198 with HTTP; Wed, 23 Aug 2017 00:18:01 -0700 (PDT) In-Reply-To: References: Date: Wed, 23 Aug 2017 09:18:01 +0200 X-Gmail-Original-Message-ID: Message-ID: To: Andreas Hennings Cc: Nikita Popov , PHP internals Content-Type: multipart/alternative; boundary="001a114415a825f39a0557668384" Subject: Re: [PHP-DEV] Contravariance and the "empty type" From: michal@brzuchalski.com (=?UTF-8?Q?Micha=C5=82_Brzuchalski?=) --001a114415a825f39a0557668384 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable Hi Andreas, 2017-08-22 21:11 GMT+02:00 Andreas Hennings : > On Tue, Aug 22, 2017 at 10:39 AM, Nikita Popov > wrote: > > On Tue, Aug 22, 2017 at 4:27 AM, Andreas Hennings > > 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 =E2=80=9Ctrue=E2=80=9D 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 > >> > >> So, would the following work? > >> > >> interface Food {..} > >> interface Fruit extends Food {..} > >> interface Banana extends Fruit {..} > >> > >> interface AbstractFoodEater { > >> function eat(INTERSECT_CHILDREN $food); > >> } > >> > >> interface AbstractFruitEater extends AbstractFoodEater { > >> function eat(INTERSECT_CHILDREN $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 relevan= t. > >> https://wiki.php.net/rfc/generics > >> > >> interface FruitEater { > >> function eat(FruitType $fruit); > >> } > >> > >> // This is not really necessary. > >> interface BananaEater extends FruitEater { > >> 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 would be something like this: > > > // Base interface for eaters that only eat a specific fruit type. > interface AbstractSpecificFruitEater { > function eat(INTERSECT_CHILDREN $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 =3D []; > public function addSpecificEater(AbstractSpecificFruitEater $eater) { > $paramClass =3D (new > \ReflectionObject($eater))->getMethod('eat')-> > getParameters()[0]->getClass(); > $this->eaters[$paramClass] =3D $eater; > } > public function eat(Fruit $fruit) { > if (null !=3D=3D $specificEater =3D $this->findSuitableEater($fruit))= { > $specificEater->eat($fruit); > return true; > } > else { > return false; > } > } > private function findSuitableEater(Fruit $fruit) { > foreach ($this->eaters as $paramClass =3D> $eater) { > if ($fruit instanceof $paramClass) { > return $eater; > } > } > } > } > > > Without the EMPTY_TYPE or INTERSECT_CHILDREN, 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 > > --=20 regards / pozdrawiam, -- Micha=C5=82 Brzuchalski about.me/brzuchal brzuchalski.com --001a114415a825f39a0557668384--