Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:120083 Return-Path: Delivered-To: mailing list internals@lists.php.net Received: (qmail 19796 invoked from network); 20 Apr 2023 17:25:21 -0000 Received: from unknown (HELO php-smtp4.php.net) (45.112.84.5) by pb1.pair.com with SMTP; 20 Apr 2023 17:25:21 -0000 Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 2A8451804C6 for ; Thu, 20 Apr 2023 10:25:20 -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=-2.8 required=5.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,RCVD_IN_DNSWL_LOW, RCVD_IN_MSPIKE_H2,SPF_HELO_PASS,SPF_NONE,T_SCC_BODY_TEXT_LINE autolearn=no autolearn_force=no version=3.4.2 X-Spam-ASN: AS19151 66.111.4.0/24 X-Spam-Virus: No X-Envelope-From: Received: from out3-smtp.messagingengine.com (out3-smtp.messagingengine.com [66.111.4.27]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange ECDHE (P-256) server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by php-smtp4.php.net (Postfix) with ESMTPS for ; Thu, 20 Apr 2023 10:25:19 -0700 (PDT) Received: from compute4.internal (compute4.nyi.internal [10.202.2.44]) by mailout.nyi.internal (Postfix) with ESMTP id 6C6055C012C for ; Thu, 20 Apr 2023 13:25:18 -0400 (EDT) Received: from imap50 ([10.202.2.100]) by compute4.internal (MEProxy); Thu, 20 Apr 2023 13:25:18 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= garfieldtech.com; h=cc:content-type:content-type:date:date:from :from:in-reply-to:message-id:mime-version:reply-to:sender :subject:subject:to:to; s=fm3; t=1682011518; x=1682097918; bh=pN 8fNes9iE19zX3HoW5fnWw9aotx2TLYRWJj2N6dq6c=; b=nFp/i71GXdOtO61qe9 VI35MmTJfzaQi9+y3eHuE4oQEZ1f59OATj/4/OrQXAFGG0m4R5BoK91RwIQRchkC abgkeE7tMvr9Yr0dIs19Rx7o0JTcLZy1tBIBg1/VuQ1+zKzzK+L0JaammSQhzney S5oAIIhm/x06PHPz916bJb2tc/ObzJb7hiaLtatmc2Z/C+N2wKd5BCf6VKVO/634 hcxtSrsK7ixN5Jgrh5sTx4ttIr8KUlAO6EGVpuPXHXRxoiGvwJpMct35gp8TAHf+ Ho67+rLyNfPBf5ojm7r/4IDLEIvcWBtNSQJJDblLS6faZ/Do5TOOirFibOEjeYEQ ixmw== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:content-type:content-type:date:date :feedback-id:feedback-id:from:from:in-reply-to:message-id :mime-version:reply-to:sender:subject:subject:to:to:x-me-proxy :x-me-proxy:x-me-sender:x-me-sender:x-sasl-enc; s=fm3; t= 1682011518; x=1682097918; bh=pN8fNes9iE19zX3HoW5fnWw9aotx2TLYRWJ j2N6dq6c=; b=ko3gB4rl07nIuWu6jQwLgWou7mwH+000VtOd/LUd8ktAij3Clr4 qGHY4aUgb2tpEHTnjUupClF1C4fZqBQrmRsi8BrHOLew/kYabrVE5mfmDaXteJ2G MtCP1Jk2i5hlS3PW9yJbWfswXAJAAk8to7685DXpam2xk1ZTB+2eierLs6fBz/TJ f41zE5mMv0lDlrn5qlQzve1WDeSpm93v7w7rMA8cky0xLvzYZ6I/Ry+kGsWAIv9o c59G6Bp/5JdYsJKVGNWrMTfqOe2XFlzJw1VGQErU9ojkHZEGWfNjnCXNRRqB1OL6 sFvFscDzGep8KRreNYpDBM3OvJSXDpriiQw== X-ME-Sender: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedvhedrfedtvddgudduudcutefuodetggdotefrod ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfgfvfdpuffrtefokffrpgfnqfgh necuuegrihhlohhuthemuceftddtnecusecvtfgvtghiphhivghnthhsucdlqddutddtmd enucfjughrpefofgggkfffhffvufgtsehttdertderredtnecuhfhrohhmpedfnfgrrhhr hicuifgrrhhfihgvlhgufdcuoehlrghrrhihsehgrghrfhhivghlughtvggthhdrtghomh eqnecuggftrfgrthhtvghrnhepgeehffeihfdtkeefveffueeiiefhjeduhfeuhfdtteel lefhvdejteekgffgueeinecuffhomhgrihhnpehphhhprdhnvghtnecuvehluhhsthgvrh fuihiivgeptdenucfrrghrrghmpehmrghilhhfrhhomheplhgrrhhrhiesghgrrhhfihgv lhguthgvtghhrdgtohhm X-ME-Proxy: Feedback-ID: i8414410d:Fastmail Received: by mailuser.nyi.internal (Postfix, from userid 501) id 190D61700090; Thu, 20 Apr 2023 13:25:18 -0400 (EDT) X-Mailer: MessagingEngine.com Webmail Interface User-Agent: Cyrus-JMAP/3.9.0-alpha0-372-g43825cb665-fm-20230411.003-g43825cb6 Mime-Version: 1.0 Message-ID: <6b5de716-d769-4f0b-b3e6-5a5a211f035a@app.fastmail.com> Date: Thu, 20 Apr 2023 17:24:57 +0000 To: "php internals" Content-Type: text/plain Subject: [Discussion] Callable types via Interfaces From: larry@garfieldtech.com ("Larry Garfield") Hi folks. This is a pre-discussion, in a sense, before a formal RFC. Nicolas Grekas and I have been kicking around some ideas for how to address the desire for typed callables, and have several overlapping concepts to consider. Before going down the rabbit hole on any of them we want to gauge the general feeling about the approaches to see what is worth pursuing. We have three "brain dump" RFCs on this topic, although these are all still in super-duper early stages so don't sweat the details in them at this point. We just want to discuss the basic concepts, which I have laid out below. https://wiki.php.net/rfc/allow_casting_closures_into_single-method_interface_implementations https://wiki.php.net/rfc/allow-closures-to-declare-interfaces-they-implement https://wiki.php.net/rfc/structural-typing-for-closures ## The problem function takeTwo(callable $c): int { return $c(1, 2); } Right now, we have no way to statically enforce that $c is a callable that takes 2 ints and returns an int. We can document it, but that's it. There is one loophole, in that an interface may require an __invoke() method: interface TwoInts { public function __invoke(int $a, int $b): int; } And then a class may implement TwoInts, and takeTwo() can type against TwoInts. However, that works only for classes, which are naturally considerably more verbose than a simple closure and represent only a subset of the possible callable types. The usual discussion has involved a way to specify a callable type's signature, like so: function takeTwo(callable(int $a, int $b): int $c) { return $c(1, 2); } But that runs quickly into the problem of verbosity, reusability, and type aliases, and the discussion usually dies there. ## The alternative What we propose is to instead lean into the interface approach. Specifically, recall that all closures in PHP are actually implemented as classes in the engine. That is: $f = fn(int $x, int $y): int => $x + $y; actually turns into (approximately) this in the engine: $f = new class extends \Closure { public function __invoke(int $x, int $y): int { return $x + $y; } } (It doesn't do syntax translation but that's effectively what the engine does.) So all that's really missing is a way for arbitrary closures to denote that they implement an interface, and then they can be used wherever an interface is required. That neatly sidesteps the verbosity and reusability issues, and since interfaces are already well-understood there's no need to wait for type aliases. It would not support the old-style funky callables like a function string or [$obj, 'method'], but with the advent of first-class-callables those are no longer recommended anyway so not supporting them is probably a good thing. The same would also work for property types, which can easily type against an interface. That would mostly sidestep the current limitation of typing a property as `callable`, since you could provide a more-specific type instead for a double-win. ## The options There's three ways we've come up with that this design could be implemented. In concept they're not mutually exclusive, so we could do one, two, or three of these. Figuring out which approach would get the most support is the purpose of this thread. ### castTo The first is to add a castTo() method to Closure. That would produce a new object that has the same logic as the closure, but explicitly implements the interface. That is, this: $fn2 = $fn->castTo(TwoInts::class); Is roughly logically equivalent to: $fn2 = new class($fn) implements TwoInts { public function __construct(private callable $fn) {} public function __invoke(int $a, int $b): int { return $this->fn(func_get_args(); } }; (Whether that's what the implementation actually does or if it's smarter about it is an open question.) In theory, this would also support any single-method interface, not just those using __invoke(). The other options below would not support that. This does have a number of open edge cases, like what to do with a closure that is already bound to an object. ### Function interfaces The second option is to allow closures to declare up front what interfaces they implement. So: $f = fn(int $x, int $y): int implements TwoInts => $x + $y; This has the advantage of being more statically analyzable (both visually and for parsers). It may also be more performant (in theory), as it could translate almost trivially to: $f = new class extends \Closure implements TwoInts { public function __invoke(int $x, int $y): int { return $x + $y; } } The downside is that it only works for user-defined closures that declare their support up-front, statically. Something like strlen(...) or strtr(...) wouldn't work. It's also a bit verbose, though using bindTo() directly on the closure is of similar length: $f = (fn(int $x, int $y): int => $x + $y)->bindTo(TwoInts::class); ### Structural typing for closures The third option would necessitate having similar logic in the engine to the first. In this case, we take a "structural typing" approach to closures; that is, "if the types match at runtime, it must be OK." This is probably closest to the earlier proposals for a `callable(int $x, int $y): int` syntax (which would by necessity have to be structural), but essentially uses interfaces as the type alias. function takeTwo(TwoInts $c): int { return $c(1, 2); } $result = takeTwo(fn(int $x, int $y): int => $x + $y); In this approach, no up-front work is needed. A callable/closure that conforms to an interface with __invoke() "just works" when it's used. Essentially this would involve detecting that the argument is a callable and the parameter is an interface with __invoke(), then trying to castTo() that interface. If it works, pass the result. If not, fail in some way. This approach would support any arbitrary closure, including strtr(...) style FCC closures. A closure would not need to pre-declare its support ("nominal typing"), which makes it more flexible. Callables "just work." The downside here is complexity. Currently, class type conformance is determined ahead of time, and a type check is just a lookup on a list on the class. This would necessitate loading the interface (possibly autoloading it) within the function call action, attempting the cast operation, and handling the potential fault. It also means it would only happen at the function boundary or property assignment; a closure would never work with instanceof, class_implements, etc., because to those it would still be "just" a \Closure. A callable syntax literal (as previously discussed) would have most of the same challenges. ## The discussion So those are the options. We feel that the interface-based approach is strong, and a good way forward for getting typed callables without a bunch of dependent features needed first. These three ways of getting there are all potentially viable (give or take implementation details and edge cases in all cases, as always), all have their own pros and cons, and in concept we could very easily adopt more than one, or combine them into a single RFC. Before we dig into any of those edge cases, however, we want to throw the question out: Is this general approach even acceptable? Are there implementation challenges to any of them we're not seeing? Would you vote for any or all of these proposals or oppose on principle? Are you interested in helping us implement any of them :-) ? Please discuss, so we can decide how to proceed toward a real concrete proposal. -- Larry Garfield larry@garfieldtech.com