On Sun, Aug 25, 2024, at 20:46, Rowan Tommins [IMSoP] wrote:
> On 25/08/2024 18:44, John Bafford wrote:
> 
>> Although I'm not sold on the idea of using default as part of an 
>> expression, I would argue that a default function parameter value is 
>> fair game to be read and manipulated by callers. If the default value 
>> was intended to be private, it shouldn't be in the function declaration.
> 
> 
> There's an easy argument against this interpretation: child classes can 
> freely change the default value for a parameter, as long as they do not make 
> it mandatory. https://3v4l.org/SEsRm
> 
> That matches my intuition: that the public API, as a contract, states that 
> the parameter is optional; the specification of what happens when it is not 
> provided is an implementation detail.
> 
> For comparison, consider constructor property promotion; the caller shouldn't 
> know or care whether a class is defined as:
> 
> public function __construct(private int $bar) {}
> 
> or:
> 
> 
> private int $my_bar;
> public function __construct(int $bar) { $this->my_bar = $bar; }
> 
> The syntax sits in the function signature because it's convenient, not 
> because it's part of the API.
> 
> 
> 
>> One important case where reading the default value could be important is
>>  in interoperability with different library versions. For example, a 
>> library might change a default parameter value between versions. If 
>> you're using the library, and want to support both versions, you might 
>> both not want to set the value, and yet also care what the default value
>>  is from the standpoint of knowing what to expect out of the function.
> 
> 
> This seems contradictory to me. If you use the default, you're telling the 
> library that you don't care about that parameter, and trust it to provide a 
> default.
> 
> If you want to know what the library did with its arguments, reflecting the 
> signature will never be enough anyway. For example, it's quite common to 
> write code like this:
> 
> 
> function foo(?SomethingInterface $blah = null) {
>     if ( $blah === null ) {
>         $blah = self::_setup_default_blah();
>     }
>     // ...
> }
> 
> A caller can't tell by looking at the signature that a new version of the 
> library has changed what _setup_default_blah() returns. If the library 
> doesn't provide an API to get $blah out later, then it's a private detail 
> that the caller has no business inspecting.
> 
> 
> 
> Regards,
> 
> -- 
> Rowan Tommins
> [IMSoP]

I think you've hit an interesting point here, but probably not what you 
intended.

For example, let's consider this function:

json_encode(mixed $value, int $flags = 0, int $depth = 512): string|false

Already, you have to look up the default value of depth or set it to something 
that makes sense, as well as $flags. So you do this:

json_encode($value, JSON_THROW_ON_ERROR, 512);

You are doing this even when you omit the default. If you set it to a variable 
to spell it out:

$default_flags = 0 | JSON_THROW_ON_ERROR;
$default_depth = 512; // according to docs on DATE

json_encode($value, $default_flags, $default_depth);

Can now be rewritten:

json_encode($value, $default_flags = default | JSON_THROW_ON_ERROR, 
$default_depth = default);

This isn't just reflection, this is saving me from having to look up the 
docs/implementation and hardcode values. The implementation is free to change 
them, and my code will "just work."

Now, let's look at a more non-trivial case from some real-life use-cases, in 
the form of a plausible story:


public function __construct(
    private LoggerInterface|null $logger = null,
    private string|null $name = null,
    Level|null $level = null,
)

This code constructs a new logger composed from an already existing logger. 
When constructing it, I may look up what the default values are and decide if I 
want to override them or not. Otherwise, I will leave it as null.

A coworker and I got to talking about this interface. It kind of sucks, and we 
don't like it. It's been around for ages, so we are worried about changing it. 
Specifically, we are wondering if we should use SuperNullLogger as the default 
instead of null (which happens to just create a NullLogger a few lines later). 
We are pretty sure making this change won't cause any issues, but to be extra 
safe, we will do it only on a single code path; further, we are 100% sure we 
are going to change this signature, so we need to do it in a forward-compatible 
way. Thus, we will set it to SuperNullLogger if-and-only-if the default value 
is null:

default ?? new SuperNullLogger()

Now, we can run this in production and see how well it performs. Incidentally, 
we discover that NullLogger implementation is superior and we can now change 
the default:

public function __construct(
    private LoggerInterface $logger = new NullLogger(),
    private string|null $name = null,
    Level|null $level = null,
)

That one code path "magically" updates as soon as the library is updated, 
without having to make further changes. Anything that is hardcoded "null" will 
break in tests/static analysis, making it easy to locate. Further, we can test 
other types of NullLoggers just as easily:

default instanceof NullLogger ? new BasicNullLogger() : default

So, yes, I think in isolation the feature might look strange, and some 
operations might look nonsensical, but I believe there is a use case here that 
was previously rather hard to do; or statically done via someone looking up 
some documentation/code and doing a search-and-replace.

— Rob

Reply via email to