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

Reply via email to