Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:110014 Return-Path: Delivered-To: mailing list internals@lists.php.net Received: (qmail 84513 invoked from network); 5 May 2020 18:52:57 -0000 Received: from unknown (HELO php-smtp4.php.net) (45.112.84.5) by pb1.pair.com with SMTP; 5 May 2020 18:52:57 -0000 Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 42D8B1804F8 for ; Tue, 5 May 2020 10:27:56 -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.3 required=5.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,RDNS_NONE,SPF_HELO_NONE, SPF_PASS autolearn=no autolearn_force=no version=3.4.2 X-Spam-ASN: AS701 96.241.0.0/16 X-Spam-Virus: No X-Envelope-From: Received: from nebula.zort.net (unknown [96.241.205.3]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by php-smtp4.php.net (Postfix) with ESMTPS for ; Tue, 5 May 2020 10:27:55 -0700 (PDT) Received: from [10.0.1.2] (pulsar.zort.net [96.241.205.6]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by nebula.zort.net (Postfix) with ESMTPSA id EE7CA2005E81B; Tue, 5 May 2020 13:27:54 -0400 (EDT) DKIM-Filter: OpenDKIM Filter v2.11.0 nebula.zort.net EE7CA2005E81B DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=zort.net; s=zort; t=1588699675; bh=+2opL1PHPURmme2rwR8S2+a8eCKyOo3gBDJBuleZwK4=; h=Subject:From:In-Reply-To:Date:Cc:References:To:From; b=G4xgbeptn0vx+lkK9g37a/AkTPE7mK99Moy+5kFvvEhHjTi038eKBzfXxcp25Im8n gWaugSE0Wz9Gd6bobmcdMxTVxkfOxNSyJm+OaITYXlfcbtds4fb+FK2GsgmG19h3WM LAy2uZrlXr4GCnsbdO+LjTul4jdEFe1M3OlbvmPg= Content-Type: text/plain; charset=us-ascii Mime-Version: 1.0 (Mac OS X Mail 13.4 \(3608.80.23.2.2\)) In-Reply-To: Date: Tue, 5 May 2020 13:27:54 -0400 Cc: PHP internals Content-Transfer-Encoding: quoted-printable Message-ID: References: To: Nikita Popov X-Mailer: Apple Mail (2.3608.80.23.2.2) Subject: Re: [PHP-DEV] [RFC] Named arguments From: jbafford@zort.net (John Bafford) Hi Nikita, > On May 5, 2020, at 09:51, Nikita Popov wrote: >=20 > Hi internals, >=20 > I've recently started a thread on resurrecting the named arguments = proposal > (https://externals.io/message/109549), as this has come up = tangentially in > some recent discussions around attributes and around object = ergonomics. >=20 > I've now updated the old proposal on this topic, and moved it back = under > discussion: https://wiki.php.net/rfc/named_params >=20 > Relative to the last time I've proposed this around PHP 5.6 times, I = think > we're technically in a much better spot now when it comes to the = support > for internal functions, thanks to the stubs work. >=20 > I think the recent acceptance of the attributes proposal also makes = this a > good time to bring it up again, as phpdoc annotations have = historically had > support for named arguments, and this will make migration to the > language-provided attributes smoother. >=20 > Regards, > Nikita I very much do like the idea of named parameters, but I do not like the = specific proposal in the linked RFC at all. I think that treating named = parameters simply as syntactic sugar is not a good approach. If we're = going to add named parameters, I think they should actually be = significant in their own right. As such, please allow me to present an = alternative. I would propose the following opt-in mechanism, which would work by = treating the parameter names as *part of the function name*. Doing it = this way I _think_ should allow for no backwards compatibility issues = with existing code and would especially give library authors full = control over the naming and presentation of their APIs. Here is how it = would work: Let's start with an ordinary function: function foo(int $a, int $b) : void {} and then add named parameters: function foo(a: int $a, b: int $b) : void {} These are now two _completely different functions_. The first is named = 'foo'. The second is named 'foo(a:b:)'. They can both coexist in the = same namespace. This would solve two big classes of backwards = compatibility issues. First, adding the named-parameter function would not require any changes = to any existing code. If the named-parameter function were to become the = preferred version, then whatever library implements it could easily = forward calls and add a deprecation warning to the old function: function foo(int $a, int $b) : void { foo(a: $a, b: $b); } Second, by having separate public and internal parameter names, it = becomes possible to evolve APIs without breaking existing users. If we = start from our original two functions above and decide to fix the = poorly-named parameters, we could make these changes and continue to = have backwards compatibility with all existing users: function foo(hours: int $hours, minutes: int $minutes) : void {} function foo(a: int $hours, b: int $minutes) { foo(hours: = $hours, minutes: $minutes } function foo($hours, $minutes) { foo(hours: $hours, minutes: = $minutes } Changing the external parameter name counts as changing the function's = name, which means it's an entirely different function. (Which lets us = also leave the old-named function there as a fallback.) Changing the = internal parameter name has no effect to the outside, and can be changed = at will. This also solves the problem with overloading: you can change = the internal parameter names all you like, so long as you keep the = external parameter name - which is part of your declared API. Because the parameter names are part of the function name, you would = *not* be able to pick and choose which parameter names you use. If the = function declares parameter names, they must be used at the call site. Parameters are also positional. foo(a:..., b: ...) and foo(b: ..., a: = ...) are two completely different functions, by name. If you have = foo(a:b:) defined, then you cannot call it as foo(b: ..., a: ...). Both of these last two provisions are to eliminate ambiguity and create = consistency in the call site. Otherwise we could easily wind up with = confusion like in the implode and explode parameter orderings. You could allow having nameless parameters by doing this: function bar(_: int $one, two: int $two) {} bar(1, two: 2); The "nameless" parameters would still be explicitly named ("_"), but = users would not have to provide that name at the call site. It would be = permissible to mix named and unnamed parameters, though style guides = want to discourage that as there's probably few use-cases where this = makes sense: function baz(holiday: string $name, _: string $date, color: = string $color) {} baz(holiday: 'Star Wars Day', 'May 4', color: 'purple'); There might be complexity around named and unnamed overlap with this = special case: function overlap($a, $b); //nominally named 'foo' function overlap(_: $a, _: $b); //nominally named 'foo(_:_)' In which case both functions would normally be called with the exact = same syntax. We could declare that, either the second form is explicitly = disallowed; or it is allowed, but both may not exist in the same scope. = (I would prefer the latter, because it allows the function to be = explicitly referenced by symbol; see future-scope musings below.) Named parameters will still all be positional, so you could still do = something like: call_user_func_array('baz:holiday:_:color', ['Star Wars Day', = 'May 4', 'purple']); Because named parameters are part of the function name, that would also = allow us to have what would appear to be name-based overloading, so we = could have: function __construct(name: string $name, ...) {} function __construct(dto: SomeDTO $data) {} $foo =3D new Holiday(name: 'Star Wars', ...): $bar =3D new Holiday(dto: $formData); This would be very useful for collections classes, where you could have: $collection->insert($object): $collection->insert(element: $object); //same as above, but more = explicit $collection->insert(contentsOf: $anotherCollection); You could also use this to "solve" some of the more confusing php = standard library parameter ordering, entirely in userspace (or as part = of the stdlib without BC breaks), by defining new helper functions: function strpos(haystack: string $haystack, needle: $needle, = offset: int $offset =3D 0) : int {...} function strpos(needle: $needle, haystack: string $haystack, = offset: int $offset =3D 0) : int {...} or even, reimagining the naming to be more like reading a sentence: strpos($string, in: $someArray); position(of: $string, in: $someArray) str_repeat($string, count: 5); implode($array, with: $glue); explode($string, delimiter: $delimiter); This means that rather than a one-size-fits-all approach for _every_ = method in the standard library, we could, on a case-by-case basis, add = variants for where named parameters are explicitly helpful. It would not = be necessary to do this all-at-once; unaudited functions and methods = would continue to be used exactly as they always have been. New = functions could offer either named-parameters and no-named-parameters = versions, whichever is most appropriate. (There could be both, but = having two names for the same thing is probably not good for API clarity = unless one is intended as a BC shim.) The exact details of the "mangled" name of functions is not important, = so long as it is unique for each function, and human-readable and = typeable since the only way to reference a function in a dynamic context = is by a string name. For example, instead of 'foo(a:b:)', 'foo:a:b' = could be used. I specifically picked 'foo(a:b:)' because it looks like a = function call (but without values, it's unambiguously not a call), and = also opens the possibility of using that specific syntax to allow for = referring to functions by symbol, rather than by string. That is, in a = hypothetical future, you could do this: var (callable(T, T))[] $comparators =3D [ 'string': \strcmp(_:_:) \DateTime::class: self::specialDateComparator(_:_:), A::class: A::compare(_:_:), B::class: B:compare(first:second:), C::class: function($a, $b) {...}, ]; usort($array, $comparators[$eltType]); But that's definitely getting into future-scope possibilities. Thank you for your consideration. I can more fully flesh this out into = an alternative RFC if people would like. -John