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

--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php

Reply via email to