Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:109581 Return-Path: Delivered-To: mailing list internals@lists.php.net Received: (qmail 67557 invoked from network); 9 Apr 2020 14:16:32 -0000 Received: from unknown (HELO php-smtp4.php.net) (45.112.84.5) by pb1.pair.com with SMTP; 9 Apr 2020 14:16:32 -0000 Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 9E3381804C2 for ; Thu, 9 Apr 2020 05:44:54 -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.1 required=5.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,FREEMAIL_FROM,HTML_MESSAGE, RCVD_IN_DNSWL_NONE,RCVD_IN_MSPIKE_H2,SPF_HELO_NONE,SPF_PASS 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-lj1-f178.google.com (mail-lj1-f178.google.com [209.85.208.178]) (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 ; Thu, 9 Apr 2020 05:44:54 -0700 (PDT) Received: by mail-lj1-f178.google.com with SMTP id n17so11267210lji.8 for ; Thu, 09 Apr 2020 05:44:53 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=mime-version:references:in-reply-to:from:date:message-id:subject:to; bh=aJhBLw/RSCaSxUP1sIDanVWxOJVCTj34K/ibwLdP+6o=; b=D0fO2k5QdnukN7dMgd3HTIDHCiNvZ4krlaGcVg9yvvuKvyBfzKHlSCECK/UzyyH7Rs JD1i2205YRCni+UgWAoPl938glvLbHGzCaaM3iQj92c0h/wrauCO3jS+n+TCJ5AjKvbQ IdGGHV1Xa07ZGzYDJgysN+IoIfeQXsI021zkPLlgQLR0BbDyX3kdQscNYi/wDZSTTUhV n7tx+blbeYZvmJjWhhF9Cbg23u8avYbvZ9ML89JeF6bK6IBBKTxU7+H3b0fESuodpYQi 8RblGsNG4fRcFviNT1rSYbJDM8R3negvr2odH3RcFvK7r0/NLiTBtjn4pqvYaaLBrJ89 ikEQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:mime-version:references:in-reply-to:from:date :message-id:subject:to; bh=aJhBLw/RSCaSxUP1sIDanVWxOJVCTj34K/ibwLdP+6o=; b=uAEi31VcB8ymOaewQ/2aJ+ra37Di9Jgn3bIAUzyb8QrHlhT717IC/ciTmRkAmT+q4A N3d4s6STNO566ca/3Jjiy0WmG+pQQWs8Fe4Da2oCBmfqLxUFcK+NPkRoT0EDvCi5fLAZ NRDjVyCgYG3b6V+wcZhMzgv/U59kM8KWvq1445BDfLdSm8JcpQSD5UAO34gKmeCjB8OC BKRDJu8PfJL+rGqqO5KaYRVOWZJNCwCyE3+EITvSOGvRu/AzB+dsY083GsMN7+ZhaaB/ /3l4K1+dKFkIUKqm9ecANAav4fxuPR/b3zTx6rWX+Ek5Wzixc/CFec/FOV1voUlFP5jl hWYw== X-Gm-Message-State: AGi0PuYtvFfJrYUtE5fwk68QXSw4gHxff2NBWz/Jlhb1iNcp8AAvpMDb nkdpRxYs7TwJ2Ro5eyupjw1f6vdjw/2aitV6HlKlt/YrJFUPjw== X-Google-Smtp-Source: APiQypLxJzQZmM+uo4+cjj/rnjUyoXXFUjQU6xgOwY0ifqFxvErqYT3obZ7Iy2hbh+6trYqF2TRkz4pi11khKSxKreA= X-Received: by 2002:a2e:8841:: with SMTP id z1mr7983404ljj.260.1586436291439; Thu, 09 Apr 2020 05:44:51 -0700 (PDT) MIME-Version: 1.0 References: In-Reply-To: Date: Thu, 9 Apr 2020 14:44:34 +0200 Message-ID: To: PHP internals Content-Type: multipart/alternative; boundary="0000000000009c300205a2dafb05" Subject: Re: Resurrecting named parameters From: nikita.ppv@gmail.com (Nikita Popov) --0000000000009c300205a2dafb05 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable On Tue, Apr 7, 2020 at 2:44 PM Nikita Popov wrote: > Hi internals, > > It's been a few years since I originally brought up the named parameters > RFC: https://wiki.php.net/rfc/named_params > > This topic has recently come up as part of > https://externals.io/message/109220 again, in particular with the > observation that the combination of constructor parameter promotion and > named parameters will give us, as a side-effect, an ergonomic object > initialization syntax that is well-integrated with the remainder of the > language. > > I've reimplemented basic named param support for master in > https://github.com/php/php-src/pull/5357, but before going any further > here, I want to restart discussion on the various open issues that the > named parameter concept in PHP has. I think there's two primary issues: > > ## LSP checks for parameter names > > Parameter names currently have no particular significance in PHP, only > their position in the signature is important. If named parameters are > introduced (in a way that does not require opt-in at the declaration-site= ), > then parameter names become significant. This means that changing a > parameter name during inheritance constitutes an LSP violation (for > non-constructor methods). There are a number of ways in which this issue > may be approached: > > 1. Silently ignore the problem. No diagnostic is issued when a parameter > name is changed during inheritance. An error exception will be thrown whe= n > trying to use the (now) unknown parameter name. > > 2. Throw a notice if a parameter name is changed. While LSP violations ar= e > normally fatal errors (in PHP 8), we could use a lower-severity diagnosti= c > for this case, that allows code to still run, but makes developers aware = of > the problem. (It should be noted that automatic fixup of parameter names > using tooling should be fairly easy to implement.) > > 3. Allow using parameter names from the parent method, even if they have > been renamed. This makes things "just work", but seems fairly magic, and > has edge cases like a signature foo($a, $b) being changed to foo($b, $a), > where it's not possible to implicitly support both at the same time. > > 4. Make named-parameter opt-in in some fashion, so that parameter names > only need to be preserved for methods that have the opt-in marker. I'm no= t > a fan of this, as it greatly diminishes the practical usefulness of named > parameters. > I've done some due diligence on this issue, and checked how other languages with named parameters handle this, and also did some quick checks on what the fallout looks like if we add a notice. ## C# C# does something fairly peculiar. Changing the parameter name introduces a new Schr=C3=B6dinger's overload: We use "new" here to indicate that the par= ent method is hidden, and it would be if we invoked positionally, but we can actually still access the parent method by performing the call with the old parameter name. This seems pretty weird to me. ``` class Test1 { public void test(int x) { Console.WriteLine("X: {0}", x); } } class Test2 : Test1 { public new void test(int y) { Console.WriteLine("Y: {0}", y); } } public class Program { public static void Main() { Test2 test2 =3D new Test2(); test2.test(y: 42); // Y: 42 test2.test(x: 42); // X: 42 } } ``` ## Python Unsurprisingly, Python just ignores the problem. If you use the name from the parent, you get an error. ``` class Test1: def test(self, x): print('X:', x); class Test2(Test1): def test(self, y): print('Y:', y); test2 =3D Test2(); test2.test(y=3D42); // Y: 42 test2.test(x=3D42); // TypeError: test() got an unexpected keyword argument 'x' ``` ## Ruby Similarly, Ruby also ignores the issue. It should be noted though that Ruby distinguishes keyword arguments from positional arguments, so you need to opt-in to their use (and if you do, they must be used at the call-site). ``` class Test1 def test(x:) print('X: ', x) end end class Test2 < Test1 def test(y:) print('Y: ', y) end end test2 =3D Test2.new; test2.test(y: 42) // Y: 42 test2.test(x: 42) // missing keyword: y ``` ## Kotlin Kotlin does what is reasonable: There is a warning about the parameter name change at the declaration-site, and a compile-error at the call-site. ``` open class Test1 { open fun test(x: Int) { println("X: $x") } } class Test2 : Test1() { // Warning: the corresponding parameter in the supertype 'Test1' is named 'x'. // This may cause problems when calling this function with named arguments. override fun test(y: Int) { println("Y: $y") } } fun main() { val test2 =3D Test2() test2.test(y=3D42); // Y: 42 // Error: Cannot find a parameter with this name: x test2.test(x=3D42); } ``` ## Swift Swift behaves somewhat similarly to C#, in that by default changing the parameter name introduces a new overload. In this case it's a proper overload though, as named parameters must be used at the call-site, so there is no method hiding going on. ``` class Test1 { func test(x: Int) { print("X: ", x) } } class Test2 : Test1 { func test(y: Int) { print("Y: ", y) } } let test2 =3D Test2() test2.test(x: 42) test2.test(y: 42) ``` When specifying that an override is desired instead, an error is generated: ``` class Test2 : Test1 { // error: argument labels for method 'test(y:)' // do not match those of overridden method 'test(x:)' override func test(y: Int) { print("Y: ", y) } } ``` Swift also distinguishes between the internal and the exernal parameter names, so it's possible to rename the internal name, while keeping the external one: ``` class Test2 : Test1 { override func test(x y: Int) { print("Y: ", y) } } ``` ## Internal methods When adding a notice for changed parameter names, methods defined by bundled extensions are mostly clean, with one notable exception: > PHP Notice: Parameter $offset of ArrayAccess::offsetExists() has been renamed to $object in WeakMap::offsetExists(), which may break named parameters in Unknown on line 0 > PHP Notice: Parameter $offset of ArrayAccess::offsetGet() has been renamed to $object in WeakMap::offsetGet(), which may break named parameters in Unknown on line 0 > PHP Notice: Parameter $offset of ArrayAccess::offsetSet() has been renamed to $object in WeakMap::offsetSet(), which may break named parameters in Unknown on line 0 > PHP Notice: Parameter $offset of ArrayAccess::offsetUnset() has been renamed to $object in WeakMap::offsetUnset(), which may break named parameters in Unknown on line 0 We get similar warnings for CachingIterator, ArrayObject, ArrayIterator, SplDoublyLinkedList, SplFixedArray, SplObjectStorage, Phar and PharData. The commonality is always that ArrayAccess parameters are renamed. > PHP Notice: Parameter $position of SeekableIterator::seek() has been renamed to $line_pos in SplFileObject::seek(), which may break named parameters in Unknown on line 0 This is the only occurrence that is not related to ArrayAccess. ## Userland methods When testing PHP-Parser, there are only two warnings. One comes from generated PhpUnit mocks, where codegen would just need to be fixed: > Parameter $invocationRule of PHPUnit\Framework\MockObject\MockObject::expects() has been renamed to $matcher in Mock_Parser_eaee5d81::expects(), which may break named parameters The other one is a renamed parameter in my own code: > Parameter $node of PhpParser\NodeVisitorAbstract::enterNode() has been renamed to $origNode in PhpParser\NodeVisitor\CloningVisitor::enterNode(), which may break named parameters To test a larger project, here are warnings during Laravel startup: https://gist.github.com/nikic/c624399659ef1052e8751d8bb3024bc4 There quite a few more of these. The majority of the warnings occurs because an internal method parameter was renamed, not a userland parameter. I haven't checked whether the parameters match docs or not here. From the non-internal cases, one that stood out is: > Notice: Parameter $files of Orchestra\Testbench\Contracts\TestCase::call() has been renamed to $cookies in Orchestra\Testbench\TestCase::call(), which may break named parameters in /home/nikic/repos/framework/vendor/orchestra/testbench-core/src/TestCase.ph= p on line 15 > Notice: Parameter $server of Orchestra\Testbench\Contracts\TestCase::call() has been renamed to $files in Orchestra\Testbench\TestCase::call(), which may break named parameters in /home/nikic/repos/framework/vendor/orchestra/testbench-core/src/TestCase.ph= p on line 15 > Notice: Parameter $content of Orchestra\Testbench\Contracts\TestCase::call() has been renamed to $server in Orchestra\Testbench\TestCase::call(), which may break named parameters in /home/nikic/repos/framework/vendor/orchestra/testbench-core/src/TestCase.ph= p on line 15 > Notice: Parameter $changeHistory of Orchestra\Testbench\Contracts\TestCase::call() has been renamed to $content in Orchestra\Testbench\TestCase::call(), which may break named parameters in /home/nikic/repos/framework/vendor/orchestra/testbench-core/src/TestCase.ph= p on line 15 That looks like an actual bug where the signature went out of sync. Illuminate defines: > public function call($method, $uri, $parameters =3D [], $cookies =3D [], $files =3D [], $server =3D [], $content =3D null) while Orchestra defines: > public function call($method, $uri, $parameters =3D [], $files =3D [], $server =3D [], $content =3D null, $changeHistory =3D true); Note that the $cookies parameter went missing. ## Conclusion From other languages, we have: * Python, Ruby: Parameter name changes ignored, throw error on call. * C#, Swift: Parameter name changes introduce new overload. (Unless "override" is used, in which case it is an error.) * Kotlin: Warns on parameter name change, error on call. From the warnings observed in practice, I think the ArrayAccess case is an interesting one, in that it's pretty unlikely that those methods will ever be called with named parameters (usually they are not explicitly called at all!) I think we might ultimately be best off with going for the Python/Ruby approach, at least initially, and encourage IDEs / static analyzers / style analyzers to offer inspections that check whether parameter names match. As we are retrofitting named parameters to a language where parameter names held no significance historically, being overly pedantic about this might not do us any favors, especially if we consider that languages like Python do get away with not enforcing anything in this regard. ## Internal functions > > There are two problems when it comes to named parameters and internal > functions. The first is that the internal parameter names do not always > match the parameter names in the documentation. This is nothing we can't > solve (in a hopefully mostly automated way), but is work that needs to be > done. > > The larger problem is that internal functions don't have a well-defined > concept of parameter default value. Parameter defaults are implicit in C > code, and sometimes based on argument count checks. When named parameters > are involved, one generally cannot assume that if a later (optional) > parameter is passed, all earlier (optional) parameters are passed as well= . > > While it is possible (but rather challenging) to make things work mostly > transparently with current ZPP, some functions do need to adjusted to > support named parameters, and may cause misbehavior/crashes if they are > not. As a rule of thumb, functions that make non-trivial use of > ZEND_NUM_ARGS() are affected. This is something we can address for > functions in bundled extensions, but may also affect 3rd-party extensions= . > > I think that ideally, we'd deal with internal functions the same way we d= o > for userland functions, by relying on default values that are part of the > function declaration. As part of the PHP stub work, we now have informati= on > on default values in stub files, but this information is currently not > available to the engine. > > M=C3=A1t=C3=A9 Kocsis has been working on getting this information expose= d in > Reflection, see https://github.com/php/php-src/pull/5353. In the current > form, accessing these defaults is fairly inefficient, but maybe with some > caching, we could base named parameters on this information: > > If there are no "gaps" in the passed parameters, then named parameters > will always work. If an optional parameter is not specified, but a later > parameter is, then we will fetch the default value for that parameter, an= d > pass it instead. If the parameter is optional, but has no well-defined > default (UNKNOWN in stubs), then the call is not permitted (Error: "Canno= t > skip parameter $foo with unknown default value", or similar.) > For the record, internal function default values are now available via Reflection, so the technical groundwork to go down this path is present now= . Regards, Nikita --0000000000009c300205a2dafb05--