Implicit Interfaces? (was [PHP-DEV] [RFC] Nullable intersectiontypes)

July 28, 2021 20:03 (Mike Schinkel)
> On Jul 27, 2021, at 11:02 PM, Jordan LeDoux> 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] [2] [3] [4] 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?