> On Jul 27, 2021, at 11:02 PM, Jordan LeDoux <jordan.led...@gmail.com> wrote:
> 
> Intersection types are very useful if you use composition over inheritance.
> That is, in PHP, they are most useful when you are using multiple
> interfaces and/or traits to represent different aspects of an object which
> might be present. For example, using an actual library I maintain, I have a
> concept of different number type objects.
> 
> NumberInterface - Anything that represents a cardinal number of any kind
> will share this.
> SimpleNumberInterface - Anything that represents a non-complex number will
> share this.
> DecimalInterface - Anything that is represented as a float/decimal will
> share this.
> FractionInterface - Anything that is represented with a numerator and
> denominator will share this.
> ComplexNumberInterface - Anything that has a non-zero real part and a
> non-zero imaginary part will share this.
> 
> To correctly represent the return types for, say, the add() method on
> Decimal, what I would *actually* return is something like
> NumberInterface&SimpleNumberInterface&DecimalInterface. The add() method on
> Fraction would instead return
> NumberInterface&SimpleNumberInterface&FractionInterface.
> 
> Now, internally, the add() method has a check for whether there is an xor
> relationship between real and imaginary parts of the two numbers. If there
> is, then a complex number object is returned instead. This means that to
> fully describe the return type of this function, the type would look like
> this:
> 
> function add(NumberInterface $num): NumberInterface&(
> (SimpleNumberInterface&DecimalInterface) |
> (SimpleNumberInterface&FractionInterface) | ComplexNumberInterface)
> 
> It can return any combination of these depending on the combination of
> types provided as arguments and being called. Now, if I got to just dictate
> how this was implemented from my own userland perspective, I'd provide
> typedefs and limit combination types to those. So, my ideal implementation
> would like like:
> 
> typedef DecimalType =
> NumberInterface&SimpleNumberInterface&DecimalInterface;
> typedef FractionType =
> NumberInterface&SimpleNumberInterface&FractionInterface;
> typedef ComplexType = NumberInterface&ComplexNumberInterface;
> 
> function add(DecimalType|FractionType|ComplexType $num):
> DecimalType|FractionType|ComplexType
> 
> But as I've mentioned earlier, none of this is really affected by
> nullability. To me, that adds very little (though not nothing). Since it
> accepts class types instead of classes themselves, I'd make an
> OptionalInterface that provides the tools to return a null instance that
> has useful information for the user of my library about why the object is
> "null".
> 
> Full combination types between unions and intersections is something that I
> would use heavily, but to me that means it should be implemented carefully
> and thoughtfully.
> 
> As they are currently, I would use intersection types less often, but they
> will still be useful in typed arguments.
> 
> I can provide actual github references to the code of mine that would
> change if that would be helpful, but I wanted to provide a broad example of
> how intersection types in general might be useful and how they might be
> used.

Hi Jordan:

THANK YOU for providing the first real-world example I have seen during this 
debate and RFC of where at least one person finds intersection types to be 
useful.

What this use-case clarified for me is that maybe this is an XY problem[1]?  

Maybe because we only have a hammer ("interfaces") when the hammer is not 
meeting our needs we ask for a better hammer("interfaces supporting nullable 
unions and intersections") when instead maybe we should as asking for a 
screwdriver ("implicitly implemented interfaces")?

Consider the complexity all these interfaces add, especially when every class 
that implements them must explicitly name them. This creates for a very fragile 
architecture when lots of interfaces are used, not to mention being much harder 
to read and follow the code:

- NumberInterface 
- SimpleNumberInterface
- DecimalInterface
- FractionInterface
- ComplexNumberInterface

Consider instead if we had the ability for any class whose signature matches an 
interface to be considered to have implemented that interface?  

Then for the example Jordan gave he could just create the following interface 
(this might not be the exact signature you'd choose, but roll with me on this 
for a bit):

interface AdderInterface {
    function add(int|float $x, $y):int|float;
}

Then any class that has an add() method where the signature matches could be 
said to "implement" the AdderInterface. 

For example, assuming this class:

class Foo {
    add(int|float $x, $y):int|float;
}

The following code could work:

function bar(AdderInterface $obj) {
        echo $obj->add(1,2);
}

Whereas the following code could fail:

function bar(AdderInterface $obj) {
        echo $obj->add("hello","world");
}

There are myriad of benefits to implicit vs. explicit interfaces including:

1. You can use your own interfaces in your own code and still use other's code 
that did not declare a class to implement your interface.

2. Implicit interfaces encourage smaller interfaces because they does not 
impose the burden of naming those interfaces on the classes that need to 
implement them. And "the bigger the interface, the weaker the abstraction."[2]

3. Implicit interfaces encourage serendipitous emergence of defacto-standard 
interfaces because people don't have to coordinate and agree, they just have to 
see that an interface gaining traction and then choose to satisfy it and/or 
implement it.

4. Small implicit interfaces can result it userland code become standardized as 
more people seek to make their code compatible with the small interfaces used 
the larger packages as with explicit interfaces there is less incentive to do 
so. 

5. As more code uses more of the same standardized interfaces we could expect 
to see more serendipitous occurrences of classes that can satisfy any given 
implicit interface leading to a positive feedback loop of increasing 
interoperability among libraries and other userland code.

6. As more people use more of the same defacto-standard interfaces, the 
proliferation of more and more specific interfaces is reduced thus reducing 
overall complexity in the ecosystem.

I could go on, but rather than making my long email even longer I will just 
point to two article about Go's interfaces that are implicitly implemented: 
[3][4]

And all of these points are not just conjecture, you only need to explore the 
Go ecosystem to see clear evidence of it.

===================

Further, if we take Dan Ackroyd's mentioned scenario of working with PHP-FIG to 
define "One True Cache to rule them all" it seems that their resolution was to 
create many small interfaces. They recognized that, even with a hammer, they 
could break out things into multiple small interfaces even though they had to 
burden the authors of an EverythingCache with having to declare it like so:

class EverythingCache implements 
Get,GetOrDefault,Set,SetWithTTL,GetOrGenerate,Clear {
     // implementation goes here
}

Let's take a look at their Get and Set methods.  

interface Get {
   public function get(string $key): mixed;
}

interface Set {
   public function set(string $key, mixed $value): void;
}

With explicit interfaces I would ask why they were not called CacheGet and 
CacheSet, but with implicit interfaces naming them Get and Set is actually a 
benefit.  Consider if those interfaces could be used elsewhere?  

Or better consider if that functions are already implemented elsewhere in 
libraries with exact same function signatures?

We might find an existing library with Get() and Set() methods could be used as 
a simple cache, even though it was never written explicitly with that in mind.  
Maybe a NoSQL database has such Get()/Set() methods in the PHP SDK already 
implemented?

And vice-versa; maybe a newly implemented Cache library could be used for a 
lightweight stand-in for a NoSQL database?

(Almost?) none of this serendipity could really happen as long as interfaces 
must be explicitly specified in order to implement them.  

And yes there is a chance for implementation incompatibility even though 
signatures match, but in practice it is never really a problem. Or at least 
nobody in the Go ecosystem complains about it.

===================

HOWEVER, we cannot have implicit interfaces in PHP as I presented above because 
of 

1.) Backward compatibility (BC) concerns, and 
2.) There are times you actually *do* want to explicitly specify an interface.

So how could we add implicit interfaces in PHP?  

We would need some way to explicitly specify that a typed variable, parameter 
or property is implicitly an interface vs. explicitly an interface.  And while 
there might be a myriad of ways to do so here are two (2) that comes to mind:

function example(#[Satisfies(AdderInterface)] $obj) {
        echo $obj->add("hello world");
}

function example(#AdderInterface $obj) {
        echo $obj->add("hello world");
}

As I am proposing either annotation tells PHP that instead of looking to see if 
$obj explicitly implemented AdderInterface it could instead look to see if its 
methods match the methods specified in the interface. And given the smaller 
nature of implicit interfaces, that should be a rather small check.

I've wanted to call for implicit interfaces in PHP for years, but I was waiting 
for someone to present a use-case that begged for them. I think Jordon provided 
that use-case. 

Do those on the list see any reason we could not consider adding implicit 
interfaces to a future version of PHP?

-Mike
[1] https://en.wikipedia.org/wiki/XY_problem
[2] https://www.youtube.com/watch?v=PAAkCSZUG1c&t=5m17s 
[3] 
https://www.efekarakus.com/golang/2019/12/29/working-with-interfaces-in-go.html
[4] https://www.alexedwards.net/blog/interfaces-explained 

P.S. My comments ignored Deleu's mention of intersection types for different 
use-cases, but only because he(she?) did not provide any concrete example.  
Maybe he(she?) or someone else could provide examples for different use-cases?

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

Reply via email to