Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:115603 Return-Path: Delivered-To: mailing list internals@lists.php.net Received: (qmail 99563 invoked from network); 28 Jul 2021 19:36:14 -0000 Received: from unknown (HELO php-smtp4.php.net) (45.112.84.5) by pb1.pair.com with SMTP; 28 Jul 2021 19:36:14 -0000 Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id C6C32180212 for ; Wed, 28 Jul 2021 13:03:32 -0700 (PDT) X-Spam-Checker-Version: SpamAssassin 3.4.2 (2018-09-13) on php-smtp4.php.net X-Spam-Level: X-Spam-Status: No, score=-1.9 required=5.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,RCVD_IN_DNSWL_NONE,RCVD_IN_MSPIKE_H2,SPF_HELO_NONE,SPF_NONE autolearn=no autolearn_force=no version=3.4.2 X-Spam-ASN: AS15169 209.85.128.0/17 X-Spam-Virus: No X-Envelope-From: Received: from mail-qk1-f179.google.com (mail-qk1-f179.google.com [209.85.222.179]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange ECDHE (P-256) server-signature RSA-PSS (4096 bits) server-digest SHA256) (No client certificate requested) by php-smtp4.php.net (Postfix) with ESMTPS for ; Wed, 28 Jul 2021 13:03:32 -0700 (PDT) Received: by mail-qk1-f179.google.com with SMTP id 184so3522019qkh.1 for ; Wed, 28 Jul 2021 13:03:32 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=newclarity-net.20150623.gappssmtp.com; s=20150623; h=mime-version:subject:from:in-reply-to:date:cc :content-transfer-encoding:message-id:references:to; bh=qySES/En3LIWYtWmxdTVpsrU67Tx2BeYBfQFWdlTW6s=; b=HIFciBA3zu3R+hmE9dpfFemAbbdwjcXAVTsfYUyeHeCGrtz+CNFLNHdYAWhuNUKwhn tE7jn3/qj1kZ4vL954GbrWyPu/R62WlI5ST0yflbCzbUg50edPgQS3zIZcmvaLiGUECY TiXfBkL9ctD9za/RBzYLcHCEMZevXeIEy02Hsgh2HBYZI8RRcfQ7L5MI2ya12QskTlgj WoW/Wf6LMMprB04XaZhRAz+24wkfp15WPRKXsHgN9qgdym+OLG4VaUH2hbV1StL0koH+ lrcIzezZ4D7eEAMTFBt5SaCN2zOJawdyS/yTuCJ5jRg+9LCcDd0r7PC32A4yRlFjbD9+ 22Gw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:mime-version:subject:from:in-reply-to:date:cc :content-transfer-encoding:message-id:references:to; bh=qySES/En3LIWYtWmxdTVpsrU67Tx2BeYBfQFWdlTW6s=; b=eURgUvFHc58BOHeQYIQt5G1Pwbaaj9VMV3MSed5R+KyrzzLBAHThCzCMipRJukdKJ3 5qG8GsTibT+JQTlmmdYptlnurCzLCdlu2T0dueTocID9kA2VUjHbV8lv5U8GBplpQmwB WxJCoaX877TbWpvzjli/72o+UvYnO4nHEQCdhZFzEB9iZzGYVuXjju1eU68iswdyr6kz r4CGZvhNsfbU2heg55lTry1oJfFmAtjnrGz5mYvrFSmc9Rf7UWKq7RHstvCze+OxUEfc 8Iho8tMy/VSnpWklHzswXTbseLit2MMkJT8qCfbQ59AQF4OWje/1P352hHKYoWJ/70ng BSeQ== X-Gm-Message-State: AOAM531gIuvOMo+fIkDhf8vfrA0uCLDwB0N2xneHR8RKpv/U8fyaN3z/ gRnX03lPkDiSXd07kOWUgorrTQ== X-Google-Smtp-Source: ABdhPJwTKimLTJkVnnWl4rmoBlVSN+VsaSjaZc8ewKOACr/kv2rXxIkDBGhPZkVghTyHwXLDRxlKYg== X-Received: by 2002:a05:620a:13f8:: with SMTP id h24mr1477371qkl.350.1627502609992; Wed, 28 Jul 2021 13:03:29 -0700 (PDT) Received: from [192.168.1.10] (c-24-98-254-8.hsd1.ga.comcast.net. [24.98.254.8]) by smtp.gmail.com with ESMTPSA id q8sm396056qtn.42.2021.07.28.13.03.29 (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128); Wed, 28 Jul 2021 13:03:29 -0700 (PDT) Content-Type: text/plain; charset=us-ascii Mime-Version: 1.0 (Mac OS X Mail 13.4 \(3608.120.23.2.7\)) In-Reply-To: <57E4430B-10E6-41AB-AFD0-318F4E7F1AC4@newclarity.net> Date: Wed, 28 Jul 2021 16:03:29 -0400 Cc: PHP internals Content-Transfer-Encoding: quoted-printable Message-ID: References: <002e01d78326$27ed0680$77c71380$@webkr.de> <57E4430B-10E6-41AB-AFD0-318F4E7F1AC4@newclarity.net> To: Jordan LeDoux X-Mailer: Apple Mail (2.3608.120.23.2.7) Subject: Implicit Interfaces? (was [PHP-DEV] [RFC] Nullable intersection types) From: mike@newclarity.net (Mike Schinkel) > On Jul 27, 2021, at 11:02 PM, Jordan LeDoux = wrote: >=20 > 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. >=20 > 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. >=20 > 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. >=20 > 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: >=20 > function add(NumberInterface $num): NumberInterface&( > (SimpleNumberInterface&DecimalInterface) | > (SimpleNumberInterface&FractionInterface) | ComplexNumberInterface) >=20 > 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: >=20 > typedef DecimalType =3D > NumberInterface&SimpleNumberInterface&DecimalInterface; > typedef FractionType =3D > NumberInterface&SimpleNumberInterface&FractionInterface; > typedef ComplexType =3D NumberInterface&ComplexNumberInterface; >=20 > function add(DecimalType|FractionType|ComplexType $num): > DecimalType|FractionType|ComplexType >=20 > 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". >=20 > 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. >=20 > As they are currently, I would use intersection types less often, but = they > will still be useful in typed arguments. >=20 > 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]? =20 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=20 - 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? =20 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.=20 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.=20 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. =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D 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. =20 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? =20 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. =20 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. =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D HOWEVER, we cannot have implicit interfaces in PHP as I presented above = because of=20 1.) Backward compatibility (BC) concerns, and=20 2.) There are times you actually *do* want to explicitly specify an = interface. So how could we add implicit interfaces in PHP? =20 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.=20 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=3DPAAkCSZUG1c&t=3D5m17s=20 [3] = https://www.efekarakus.com/golang/2019/12/29/working-with-interfaces-in-go= .html [4] https://www.alexedwards.net/blog/interfaces-explained=20 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?