Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:100279 Return-Path: Mailing-List: contact internals-help@lists.php.net; run by ezmlm Delivered-To: mailing list internals@lists.php.net Received: (qmail 29973 invoked from network); 22 Aug 2017 19:11:45 -0000 Received: from unknown (HELO lists.php.net) (127.0.0.1) by localhost with SMTP; 22 Aug 2017 19:11:45 -0000 Authentication-Results: pb1.pair.com smtp.mail=andreas@dqxtech.net; spf=permerror; sender-id=unknown Authentication-Results: pb1.pair.com header.from=andreas@dqxtech.net; sender-id=unknown Received-SPF: error (pb1.pair.com: domain dqxtech.net from 209.85.223.171 cause and error) X-PHP-List-Original-Sender: andreas@dqxtech.net X-Host-Fingerprint: 209.85.223.171 mail-io0-f171.google.com Received: from [209.85.223.171] ([209.85.223.171:37695] helo=mail-io0-f171.google.com) by pb1.pair.com (ecelerity 2.1.1.9-wez r(12769M)) with ESMTP id B8/C7-34801-0F18C995 for ; Tue, 22 Aug 2017 15:11:45 -0400 Received: by mail-io0-f171.google.com with SMTP id y4so202958iod.4 for ; Tue, 22 Aug 2017 12:11:44 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=dqxtech-net.20150623.gappssmtp.com; s=20150623; h=mime-version:in-reply-to:references:from:date:message-id:subject:to :cc:content-transfer-encoding; bh=yww+IdBxZa6Ex8bha2/jwZhXgFtIhfNEy6puPQOtjiY=; b=vUN0oPC8luo9wewBRgXKtnV/rs4O1vuY8SYSrf513U+t+xXmYTOOOozsBeanvni2dM FGIzPz0d7xwJL69uS6QU4s0i3EQlLw2o6eNFguYK8kZ0exZbATI7MEzh5fGALYziLCRn JPAoPRaGUNLM3EW9/uZ894ipjrbA6rRsZcodIIqLMlZP240WXZfpScVCyCiAc+qfkBEA Hy2D74M7VESAcrVSsU4dhfnXwUpiIJ07ezaLBELpFioWzfA3636dlZNFfRe0M2UDgNyP RrmSzyYOA3gLrcX/QXaLlXwN8l5WLgDVJOqBfagJxwHKIyYU59VDj3Uwd3Oj/jtsdEVP xT7Q== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:mime-version:in-reply-to:references:from:date :message-id:subject:to:cc:content-transfer-encoding; bh=yww+IdBxZa6Ex8bha2/jwZhXgFtIhfNEy6puPQOtjiY=; b=jjTAHbzH8YNYPxB1vbcel+5WpxhnsGB4VjaEqVIuTl0SNAHN+50iSxdfQSCGLMRuyq sQxqEtba7kUJz+xTBlB2ASPDo0OtGspM6PsgFWWhI6mfa05hZ8RyjIYP5XcTLY4YnaYS 22aSCCd9s22q2EaDZj9SoO1BHyBJli62u9PHx1WNrDV+V776yheUZi6i16SHqsa/aod0 Bz3s//LCU5xPqXIlvabGoJtJchElckoDG5v6QwXE/t7VaHTx+xuDkG6tQ3mPdsj4Y/g7 35Qvc3dJx5y6Yf/RIW/KKX+1qSe4mLmXp+f/JMWFXucyqKzoZWujhK3mJSYK2SEYEryF 6Rug== X-Gm-Message-State: AHYfb5g/blWRDfWVxZqI1p8O7qSPju64nMvED7SV7SWSIhie3EYH236o dd5BYtIueDnJFtMX0no= X-Received: by 10.107.156.147 with SMTP id f141mr177124ioe.78.1503429101575; Tue, 22 Aug 2017 12:11:41 -0700 (PDT) Received: from mail-it0-f42.google.com (mail-it0-f42.google.com. [209.85.214.42]) by smtp.googlemail.com with ESMTPSA id z64sm3974383ioi.65.2017.08.22.12.11.40 for (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128); Tue, 22 Aug 2017 12:11:40 -0700 (PDT) Received: by mail-it0-f42.google.com with SMTP id n5so6938102itb.0 for ; Tue, 22 Aug 2017 12:11:40 -0700 (PDT) X-Received: by 10.36.201.70 with SMTP id h67mr829756itg.162.1503429099959; Tue, 22 Aug 2017 12:11:39 -0700 (PDT) MIME-Version: 1.0 Received: by 10.50.44.34 with HTTP; Tue, 22 Aug 2017 12:11:19 -0700 (PDT) In-Reply-To: References: Date: Tue, 22 Aug 2017 21:11:19 +0200 X-Gmail-Original-Message-ID: Message-ID: To: Nikita Popov Cc: PHP internals Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable Subject: Re: [PHP-DEV] Contravariance and the "empty type" From: andreas@dqxtech.net (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 is= n't part of this >> > RFC, as implementing that is far more difficult, and would require >> > additional rules about autoloading and/or class compilation, which mig= ht >> > 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 relevant. >> 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 aro= und > 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 Appl= e. - 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 abo= ve. 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.