Re: Explicit callee defaults

From: Date: Sat, 27 Jul 2024 07:19:10 +0000
Subject: Re: Explicit callee defaults
References: 1 2 3  Groups: php.internals 
Request: Send a blank email to [email protected] to get a copy of this message


On Sat, Jul 27, 2024, at 02:04, Mike Schinkel wrote:
> > On Jul 26, 2024, at 6:42 PM, Christoph M. Becker <[email protected]> wrote:
> > 
> > I have only skimmed your suggestion, but it sounds quite similar to
> > <https://wiki.php.net/rfc/skipparams>.
> 
> I would really love to hear from some of those who voted "no" ~9 years why they did
> so, and if they still feel the same.
> 
> > On Jul 26, 2024, at 5:54 PM, Bilge <[email protected]> wrote:
> > When writing a function, we can specify defaults for its parameters, and when calling a
> > function we can leverage those defaults implicitly by not specifying those arguments or by
> > "jumping over" some of them using named parameters. However, we cannot explicitly use the
> > defaults. But why would we want to?
> > 
> > Sometimes we want to effectively inherit the defaults of a function we're essentially
> > just proxying. One way to do that is copy and paste the entire method signature, but if the defaults
> > of the proxied method change, we're now overriding them with our own, which is not what we
> > wanted to do. It is possible, in a roundabout way, to avoid specifying the optional parameters by
> > filtering them out (as shown in the next example). The final possibility is to use reflection and
> > literally query the default value for each optional parameter, which is the most awkward and verbose
> > way to inherit defaults.
> > 
> > Let's use a concrete example for clarity.
> > 
> > function query(string $sql, int $limit = PHP_INT_MAX, int $offset = 0);
> > 
> > function myQuery(string $sql, ?int $limit = null, ?int $offset = null) {
> >     query(...array_filter(func_get_args(), fn ($arg) => $arg !== null));
> > }
> > 
> 
> Yes, I run into that issue all the time.  So much so that I adopted an "optional
> args" pattern which itself has many downsides but in my code it has had fewer downsides than
> doing it other ways.
> 
> So for your example I would write it like this (though I hate having to double-quote the names
> of the optional args):
> function query(string $sql, array $args = []);
> 
> function myQuery(string $sql, array $args = []) {
>     query($sql, $args);
> }
> 
> myQuery("SELECT * FROM table;", ["offset" => 100]));
> 
> This is very flexible and a pattern that served me well for 10+ years. 
> 
> But of course this has none of the type safety nor code-completion benefits of named
> parameters, which I would sorely like to have.
> 
> 
> > In this way we are able to filter out the null arguments to inherit the callee defaults,
> > but this code is quite ugly. Moreover, it makes the (sometimes invalid) assumption that we're
> > able to use null for all the optional arguments.
> > 
> > In my new proposal for explicit callee defaults, it would be possible to use the
> > default keyword to expressly use the default value of the callee in that argument
> > position. For example, the above implementation for myQuery() could be simplified to the following.
> > 
> > function myQuery(string $sql, ?int $limit = null, ?int $offset = null) {
> >     query($sql, $limit ?? default, $offset ?? default);
> > }
> > 
> > Furthermore, it would also be possible to "jump over" optional parameters
> > without using named parameters.
> > 
> 
> To me, while nice I feel it is very much like only a quarter step in the right direction.
> 
> I do not meaning to highjack your thread; feel free to ignore this if you feel committed to
> pursing only your stated approach and are not interested in what I would like to see.  But I mention
> in hopes you and others see value in a more complete approach:
> 
> To begin, we can already currently do this (although I do not see many people doing it in PHP;
> fyi this approach is the norm for many Go programmers):
> 
> class QueryArgs {
>    public function __construct(
>      public ?int $limit = 100,
>      public ?int $offset = 0,
>    ){}
> }
> 
> function myQuery(string $sql, QueryArgs $args = new QueryArgs()) {
>    query($sql, $args);
> }
> 
> myQuery("SELECT * FROM table;", new QueryArgs(offset:100));
> 
> I can only give my reasons for not doing it myself in PHP and speculate that others may have
> similar reasons and/or have never even considered it:
> 
> 1.  Not having an easy and consistent way to declare the "args" class in the same
> file where the functions that use it are declared which keeps it out-of-sight and makes it harder to
> keep updated than it needs to be.

There’s nothing stopping you from doing that, except your autoloader. If you wanted to have every
class ending with Arg load the same class without arg; so QueryArg and Query are usually in the same
file (but can be in separate files too), you could do something like this:

<?php
function customArgAutoloader($className) {
    if (substr($className, -3) === 'Arg') {
        $baseClass = substr($className, 0, -3);
        $filePath = __DIR__ . '/' . $baseClass . '.php';

        if (file_exists($filePath)) {
            require_once $filePath;
        } 
    }
}

spl_autoload_register('customArgAutoloader');

(Untested, but you get the idea)



— Rob


Thread (11 messages)

« previous php.internals (#124637) next »