[PHP-DEV] [RFC] Default expression

On Aug 26, 2024, at 2:26 AM, John Coggeshall <john@coggeshall.org> wrote:
The proposal in the RFC creates a new dependency and backward compatibility issue for API developers that currently does not exist. It is not just because it allows for non-sensical expressions, but that it allows perfectly sensical expressions that would create dependencies between libraries that I don't think are a worthwhile tradeoff. See my code example below.

If you reread my email you'll note I divided it into two objections and your reply seems not to recognize that.

Your comment defends against the objection I listed as #2 but you quoted the discussion of the objective listed as #1.

(TL;DR; Down the thread a bit I put together a concrete example of why I'm opposed to this RFC)

I read through the entire thread and I did not see any examples you provided that provided any real-world concerns, unless I misunderstood. But since you provided a better example let us just ignore my comments on those.

Consider this metaphor -- If I have a object with private properties, PHP doesn't allow me to reach into that object and extract those values without a lot of work (e.g. Reflection) by design. Right now, there are lots of libraries out there defining default values and right now today they are in the same sense "private" to the function/method they are defined for. This PR would change the visibility of those default values, pulling them higher up into the call stack where IMO they don't belong -- essentially making them a brand new dependency API devs need to worry about.

I acknowledged that. However, I was not convinced that the default value concern is an actual concern vs. a theoretical concern. So that is why I asked for specific examples where it would cause a real problem vs. just a theoretical concern.

Sure if I have `foo(int $index=1)`, a developer calls with `foo(default*3)`, and then the author of foo changes the signature to `foo(int $index=0)` that might cause problems, but what is a real world scenario where a developer would actually do that, the author then change it, and then is causes a non-trivial problem?

How is that not a real-world contrived scenario?

Because there was no use-case described for:

1. The purpose of the function,
2. Why someone would multiply the default times three, nor
3. Why the API developer would break BC and change the default for the use-case.

That kind of information is what I was looking for.

Besides, are you really calling a function named "foo()" a "real-world scenario?" :wink:

Anyway, I am going to skip to your example because otherwise I would just be repeating my call for concrete examples in response to your earlier comments in that reply.

Let me propose this example and see if you still hold firm to your option that the following expression would not be valid and that it still would not be a good idea for the language:
class MyDependency {...}
function doSomething(MyDependency $dep= new MyDependency) {...}
doSomething((default)->WithLogger(new Logger));

Let's make that a little more complicated so you'll see the problem -- Consider this rather lengthy example building off the concept of your example:

<snip>

(new A)->withLogger((default)->log('B'));

Well, this is not the example I asked you to comment on. Yes, you are using `WithLogger()` but you are not using it in the same context I asked about.

Nonetheless, I will consider your example. It would be nice if you would revisit mine.

This would be valid under this RFC, right?

No, because the method log() is implicitly void. WithLogger() expects a Logger, not a null. But then I expect your example just had some logic errors, no?

BTW, in your example I am not sure I understand what `log()` does. I assume it is logging a value?

But now as the author of class A I later want to change the default of my withLogger method. Instead of just passing in new DatabaseLogger, I now want to change my API default to just a LoggerType::DB enum for reasons (What the reasons aren't relevant).

Today I don't have to think too hard about that change from an API BC perspective because the consumer has either passed in a LoggerInterface or a LoggerType -- or left it empty and used it's default value. I can just change the default to LoggerType::DB and be on my way. The downstream developer will never know or care that I did it because if they wanted to call log() they had to first create their own instance of LoggerInterface and have a reference to that object in their local context like so:

$logger = LoggerType::DB->getLogger();
(new A)->withLogger($logger);
$logger->log('B');

With this RFC, now I can't change this API call without introducing a BC break in my library because I have no idea at this point if some downstream caller decided to use my default value directly or not.

Okay, this I can work with. Thank you.

From the example you gave it appears that we can have a concrete problem when:

1. There is a parameter with a default value,
2. That parameter is type-hinted,
3. The hinted type is declared as a union type,
4. An earlier version of the library initialized the default with a value having one of the union types,
5. End-user developers used the library and then use `default` as an expression of that type, and finally
6. The library developer changed the initialization of the `default` to a different type from the union.

Did I correctly identify the problematic use-case?

Let us assume I did. Clearly this would be a BC break and the type you are concerned with.

Ok, so for argument sake, what if they revise the RFC to only allow `default` to be used in an expression when the parameter is not type-hinted with a union? Would that address your concern? Or are there other use-cases that are problematic that do not hinge on the parameter being type-hinted as a union type?

Note they could also propose a default value be able to be set for each type in a union, with the first one being the default default if all else is equal. That might not be exactly to your liking, but it seems like it could address your stated problematic use-case.

BTW, you could also guard against that problem you claim you cannot guard against by ensuring your parameters with defaults are single types with no extraneous methods. In your example if you instead used a LoggerGetter interface with a single method GetLogger() you would sidestep the problem you are concerned with completely, and you might end up with a better API, too.:

public function withLogger(LoggerGetterInterface $a = new DatabaseLoggerGetter): static
{
    $this->log = $a->getLogger();
    return $this;
}

You can argue if this is a good API design or not, but it was only written to provide a real example of how pulling the default value higher up the call chain and allowing it to be used in expressions is problematic for library authors all to save a couple of lines of code on the consumer side.

FWIW, I do not see the RFCs benefit as "saving lines of code" so much as instead "improving clarity of code."

I'm honestly not sure what you're asking here. PHP currently doesn't allow you access to the "default value" of a function you are calling (maybe Reflection? I don't know offhand).

You wrote (paraphrasing) "Developers shouldn't be allowed to access defaults because they API did not explicitly allow them to." Well then, it is simple completion of the binary logic to ask "How then do they explicitly give permission?"

IOW, if the argument is it can't be accessed because of lack of explicit permission it seems maybe what is needed is an RFC to decide:

1. Should defaults just be assumed to be part of the public API, and
2. If no, then how can defaults be explicitly made public?

Maybe this?

function withLogger(Logger $a = public new Logger): Logger {...}

So yes, I am pointing out a problem but not providing a solution because I don't currently agree a solution is even needed.

Fair enough.

OTOH, depending on the number who support this (I have no idea the number) you might be in a position that you'll get what you don't want unless you can propose a solution that addresses your concerns while also meeting their needs to. Just something to consider.

On Aug 26, 2024, at 3:28 AM, Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> wrote:
I was responding to someone justifying anything and everything the proposal allows, because Reflection already allows it. If the feature was "first class syntax to access private methods of a class", I don't think it would be controversial to challenge it. Saying "Reflection can already do it" would be a poor defence, because part of Reflection's job is to break the normal rules of the language.

But saying that we can be certain default values are private and we want to keep them that way is provably false by the existence of Reflection.

Yes, I get that it is more likely people would use this `default` keyword in this way that they would use Reflection but not permitting default as expression that does not provide any more _guarantee_ they won't then they already have.

The overriding rule, in my head, is that the caller shouldn't need to know, and shouldn't be able to find out, what a particular implementation has chosen as the default. So the particularly problematic operations are things like this:

foo($whatWasTheDefault=default)
foo(logAndReturn(default))
foo(doSomethingUnrelated(default) && false?: default)

I can see people inventing use cases for these as soon as they're available, but by doing so they will be completely changing the meaning of default parameters, which currently mean "I trust the implementation to substitute something valid here, and have no interest in what it is".

Yes, that is the way it has (mostly) been, but justifying your argument that (you believe) the caller shouldn't know the default's value simply because it is currently not (generally) accessible is a poor defense for why it should not be accessible. Or as they say; live by the sword, die by the sword.

It is unrealistic to say that a developer shouldn't know anything about what a default value is because — without knowing what what the default actually is — how does a developer know the default value is the proper value for their use-case?

Given that, I think there is an argument to be made that default values *should* be made public and accessible and that developers who write functions should plan accordingly. (I am not 100% sure I buy the argument I just made yet, but I am also not sure that I do not.). Making it part of the signature would make even more behavior explicit, and in most cases that is a good thing.

Note that under inheritance, the default value may even change type:

class A { public function foo(int $bar=42) { ... } }
class B extends A { public function foo(int|string $bar='hello') { ... } }

*Arguably*, that could violate LSP. But even if not, it could violate the principle of least astonishment.

OTOH, you are still presenting abstract hypotheticals with no evidence that there exists use-case where developers would actually do what you are worried about them doing. How about providing a concrete example use-case as John Coggeshall did? Note we discovered a specific problematic use-case that might be avoided or worked-around by our collaborating in that way.

You ask how a library can provide access to that default, and the answer is generally pretty trivial: define a public constant, and refer to it in the parameter definition.

A global? Really?

Yes, you can namespace them, but that doesn't tie them to the function or the class. Too bad.

The only exception I think is "new in initializer", where you would have to provide a function/method instead of a constant, and couldn't currently reuse it in the actual signature.

Besides that Mrs Lincoln, how was the play?

Oops, sorry. I forgot you were not an American. :wink:

Aside: one of those examples brings up an interesting question: is the value pulled out by "default" calculated only once, or each time it's mentioned? In other words, would this create 3 pointers to the same object, or 3 different objects?

foo(array|Something $x=new Something);
foo([default, default, default]);

That is a question for the RFC author.

On Aug 26, 2024, at 5:03 AM, Andreas Leathley <a.leathley@gmx.net> wrote:
When I have the GzipCompression class, I would know there is a default
value for $level, but when using the interface there might or might not
be a default value, depending on the implementation. As far as I read
the RFC, using "default" when there is no default would lead to a
runtime exception, but there is no way of finding out if there is a
default if you do not already know. Being able to test that could be
useful, although I am not sure about the syntax for that.

Great catch!

On Aug 26, 2024, at 6:32 AM, Andreas Heigl <andreas@heigl.org> wrote:
I think I am missing something here. From my understanding we are *either* coding against the interface and then it should not be possible to use `default` at all as no default is set in the interface. So the fatal error is totally valid for me.

Maybe default value for method parameters in interfaces is something the RFC should consider enabling?

-Mike

On Mon, 26 Aug 2024, at 10:14, Bilge wrote:

You're absolutely right, I would be interested to see any viable patch
that effectively implements a set of restrictions on how `default` may
be used. Requesting it be done at the parser level was not meant as a
gotcha, that's just how I (with my lack of experience) would have
approached it, but certainly trapping cases in the compiler is equally,
if not more valid and/or practical.

Another approach that occurred to me was in the executor: rather than evaluating to the default value immediately, "default" could resolve to a special value, essentially wrapping the reflection parameter info. Then when the function is actually called, it would be "unboxed" and the actual value fetched, but use in any other context would be a type error.

That would allow arbitrarily complex expressions to resolve to "default", but not perform any operations on it - a bit like propagating sqrt(-1) through an engineering formula where you know it will be cancelled out eventually.

I don't know if this is practical - I'm not sure how that special value would be represented - but I thought I'd mention it in case it sparks further ideas.

On Mon, 26 Aug 2024, at 10:03, Andreas Leathley wrote:

interface CompressionInterface
{
public function compress(string $data, int $level): string;
}

class GzipCompression implements CompressionInterface
{
public function compress(string $data, int $level = 4): string
{
// do something
}
}

When I have the GzipCompression class, I would know there is a default
value for $level, but when using the interface there might or might not
be a default value, depending on the implementation.

This isn't unique to defaults; GzipCompression could also widen the type to int|string, for instance, and there's no syntax for detecting that either.

If you have access to change class GzipCompression, you can resolve this by creating an additional interface:

interface SimplifiedCompressionInterface extends CompressionInterface
{
public function compress(string $data, int $level = 4): string;
}
class GzipCompression implements SimplifiedCompressionInterface ...

Then, if we can agree an implementation, you could write:

/** @var CompressionInterface $comp */
$comp->compress($myData, $comp instanceof SimplifiedCompressionInterface ? default : MY_DEFAULT_LEVEL);

If you don't have access to change the hierarchy, then what you're probably looking for is structural typing, or implicit interfaces - i.e. a way to ask "does this object meet these criteria". For instance, some imaginary pattern matching on the signature:

/** @var CompressionInterface $comp */
$comp->compress($myData, $comp is { compress(string, optional int) } ? default : MY_DEFAULT_LEVEL);

Note how, in both cases, we're not asserting anything about the default value itself, only that the signature defines the parameter as optional. It's actually a bit of a quirk that the interface has to specify a value, rather than just stating this:

interface SimplifiedCompressionInterface extends CompressionInterface
{
public function compress(string $data, optional int $level): string;
}

Regards,
--
Rowan Tommins
[IMSoP]

On Mon, Aug 26, 2024 at 12:49 PM Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> wrote:

On Mon, 26 Aug 2024, at 10:14, Bilge wrote:

You’re absolutely right, I would be interested to see any viable patch
that effectively implements a set of restrictions on how default may
be used. Requesting it be done at the parser level was not meant as a
gotcha, that’s just how I (with my lack of experience) would have
approached it, but certainly trapping cases in the compiler is equally,
if not more valid and/or practical.

Another approach that occurred to me was in the executor: rather than evaluating to the default value immediately, “default” could resolve to a special value, essentially wrapping the reflection parameter info. Then when the function is actually called, it would be “unboxed” and the actual value fetched, but use in any other context would be a type error.

That would allow arbitrarily complex expressions to resolve to “default”, but not perform any operations on it - a bit like propagating sqrt(-1) through an engineering formula where you know it will be cancelled out eventually.

I 100% agree with this.
“default” should not evaluate to a value before sending it as an argument to the function or method.
I understand from what the RFC author wrote a while ago that doing so (evaluating to the actual default value using reflection) was the easier and perhaps only viable way at the moment, but if that’s the case, I don’t think the value of this RFC justifies doing something like that, which to me seems like a hack.

For those already expressing interest in being able to modify a binary flag default parameter using this “trick”, I still don’t think it justifies this RFC. In my opinion, functions that accept multiple arbitrary options by compressing them into a binary flag have a badly designed interface to begin with.

So, my 10c: Make “default” a pure keyword / immutable value if possible, or reconsider whether this feature is really worth the fuss.

Best,
Jakob

Hey Bilge,

On 24.08.2024 18:49, Bilge wrote:

New RFC just dropped: PHP: rfc:default_expression. I think some of you might enjoy this one. Hit me with any feedback.

Great work overall, I'm all for it and even though it's not something I saw myself using a whole lot, the json_encode example sold me on it being more useful than I initially thought.

One question (sorry if someone already asked, I scanned the thread but it is getting long..):

Taking this example from the RFC:

 function g\($p = null\) \{
     f\($p ?? default\);
 \}

Could you go one step further and use default by default but still allow null to be passed in?

 function g\($p = default\) \{
     f\($p\);
 \}

I suppose this would mean $p has to hold this "default" value until a function call is reached, at which point it would resolve to whatever the default is. This probably complicates things for very little gain but I had to ask.

Best,
Jordi

On Mon, 26 Aug 2024, at 11:43, Mike Schinkel wrote:

You ask how a library can provide access to that default, and the answer is generally pretty trivial: define a public constant, and refer to it in the parameter definition.

A global? Really?

I didn't say "global", I said "public". Since you're keen on real-world examples, here's a simplified version of a real class:

class ConfigSource
{
  private HttpClientInterface $httpClient;

  public const DEFAULT_CONSUL_URL = 'http://localhost:8500';

  public function __construct(private string $consulUrl=self::DEFAULT_CONSUL_URL, ?HttpClientInterface $httpClient=null)
  {
    if ( $httpClient === null ) {
      $httpClient = new HttpClient();
      $httpClient->setRequestTimeoutSecs(5);
      $httpClient->setConnectRequestTimeoutSecs(5);
    }
    $this->httpClient = $httpClient;
  }
}

This constructor has two optional parameters; one of them uses a default which is also referenced explicitly in another class, so is exposed as a constant; the other uses a completely opaque method of creating a default object, which even Reflection could not expose.

The caller doesn't need to know how any of this works to make use of the class. The contract is "__construct(optional string $consulUrl, optional ?HttpClientInterface $httpClient)"

The purpose of the optional parameters is so that you can write "$configSource = new ConfigSource();" and trust the library to provide you a sensible default behaviour.

If it was decided that the code for creating a default HttpClient was needed elsewhere, it could be refactored into a method, with appropriate access:

public function __construct(private string $consulUrl=self::DEFAULT_CONSUL_URL, ?HttpClientInterface $httpClient=null)
{
  $this->httpClient = $httpClient ?? $this->getDefaultHttpClient();
}

public function getDefaultHttpClient(): HttpClient
{
  $httpClient = new HttpClient();
  $httpClient->setRequestTimeoutSecs(5);
  $httpClient->setConnectRequestTimeoutSecs(5);
  return $httpClient;
}

Or perhaps the HttpClient becomes nullable internally:

public function __construct(private string $consulUrl=self::DEFAULT_CONSUL_URL, private ?HttpClientInterface $httpClient=null) {}

Or maybe we allow explicit nulls, but default to a simple class instantiation:

public function __construct(private string $consulUrl=self::DEFAULT_CONSUL_URL, private ?HttpClientInterface $httpClient=new HttpClient) {}

None of these are, currently, breaking changes - the contract remains "__construct(optional string $consulUrl, optional ?HttpClientInterface $httpClient)".

Regards,
---
Rowan Tommins
[IMSoP]

On Mon, 26 Aug 2024, at 11:43, Mike Schinkel wrote:

On Aug 26, 2024, at 3:28 AM, Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> wrote:
I was responding to someone justifying anything and everything the proposal allows, because Reflection already allows it. If the feature was "first class syntax to access private methods of a class", I don't think it would be controversial to challenge it. Saying "Reflection can already do it" would be a poor defence, because part of Reflection's job is to break the normal rules of the language.

But saying that we can be certain default values are private and we
want to keep them that way is provably false by the existence of
Reflection.

Sorry to double-reply when the thread is already quite busy, but I overlooked this sentence, and don't want to leave it unchallenged.

By this reasoning, any property or method marked "private" is actually part of the public API of a class, because it can be accessed via Reflection. Perhaps we could add a "sudo" operator to make it easier:

class Xkcd149 {
    private function make_me_a_sandwich() { ... }
}
echo (new Xkcd149)->make_me_a_sandwich(); // error
echo (new Xkcd149)->(sudo)make_me_a_sandwich(); // OK, here you go...

I'd be surprised if anyone thought that was a good idea, because people are generally quite happy to separate "is part of the language" from "is possible to do if you mess around with Reflection APIs".

Right now, accessing the default value of an optional parameter is in the "possible if you mess around with Reflection APIs" set; maybe we do want to move it to the "is part of the language" set, maybe we don't; but claiming there is no distinction is nonsense.

---
Rowan Tommins
[IMSoP]

Hi :slight_smile: Thanks! I concede, this is one of those tools for your toolbox that you will seldom reach for, but comes in very handy whenever you do. I don’t blame you. I’ll summarise the main takeaways in the RFC later.

···

On 26/08/2024 12:55, Jordi Boggiano wrote:

Hey Bilge,

On 24.08.2024 18:49, Bilge wrote:

New RFC just dropped: https://wiki.php.net/rfc/default_expression. I think some of you might enjoy this one. Hit me with any feedback.

Great work overall, I’m all for it and even though it’s not something I saw myself using a whole lot, the json_encode example sold me on it being more useful than I initially thought.

One question (sorry if someone already asked, I scanned the thread but it is getting long…):

Taking this example from the RFC:

function g($p = null) {
f($p ?? default);
}

Could you go one step further and use default by default but still allow null to be passed in?

function g($p = default) {
f($p);
}

No. The RFC has a very specific and singular focus in this regard: to permit default only in function call contexts. That is, although default is a valid expression, it cannot be passed around or stored in a variable. Since this is a function definition, rather than a call, this will result in a compiler error. The specific error we get in this case is: “Fatal error: Constant expression contains invalid operations”.

Cheers,
Bilge

On 24 Aug 2024, at 23:49, Bilge bilge@scriptfusion.com wrote:

Hi gang,

New RFC just dropped: PHP: rfc:default_expression. I think some of you might enjoy this one. Hit me with any feedback.

This one already comes complete with working implementation that I’ve been cooking for a little while. Considering I don’t know C or PHP internals, one might think implementing this feature would be prohibitively difficult, but considering the amount of help and guidance I received from Ilija, Bob and others, it would be truer to say it would have been more difficult to fail! Huge thanks to them.

Cheers,
Bilge

Hi,

I noticed someone talking about the various ways the default keyword could be used in an expression including in match() and looking in the RFC examples I see it is listed, so I think it’s useful to clarify here. I haven’t followed the entire thread in depth so I apologise if this was already answered, but I haven’t noticed it being mentioned/clarified yet.

Can you clarify in the following, is the arm comparing against match’s default or the parameter’s default? Or to put it another way, in the second call, If $arg is 2, will the match error out due to an unmatched subject, or will it pass 1 to F?

function F(int $foo = 1) {}

F(match(default) { default => default });



```
F(match($arg) { 'a' => 0, default => default });
```

Cheers

Stephen

On Mon, Aug 26, 2024 at 5:36 AM, Jakob Givoni <[jakob@givoni.dk](mailto:On Mon, Aug 26, 2024 at 5:36 AM, Jakob Givoni <<a href=)> wrote

"default" should not evaluate to a value before sending it as an argument to the function or method.

I have no dog in this fight, but I agree with the above. Plus, having thunks like that also opens up the gateway to hell^w^w^w^wmany other possibilities such as Scala-style lazy arguments, futures, who knows what else…

—c

I guess what the opponents to this RFC are zeroing on in this thread is the conclusion that default (as proposed) is a form of contravariant return, and thus breaks Liskov Substitution Principle. Your suggestion of making it an opaque value that cannot be read outside of the called function is a nice (and maybe the only) way to resolve this problem.

···

Best regards,
Bruce Weirdan mailto:weirdan@gmail.com

On Mon, Aug 26, 2024, at 6:36 AM, Jakob Givoni wrote:

On Mon, Aug 26, 2024 at 12:49 PM Rowan Tommins [IMSoP]
<imsop.php@rwec.co.uk> wrote:

On Mon, 26 Aug 2024, at 10:14, Bilge wrote:
> You're absolutely right, I would be interested to see any viable patch
> that effectively implements a set of restrictions on how `default` may
> be used. Requesting it be done at the parser level was not meant as a
> gotcha, that's just how I (with my lack of experience) would have
> approached it, but certainly trapping cases in the compiler is equally,
> if not more valid and/or practical.

Another approach that occurred to me was in the executor: rather than evaluating to the default value immediately, "default" could resolve to a special value, essentially wrapping the reflection parameter info. Then when the function is actually called, it would be "unboxed" and the actual value fetched, but use in any other context would be a type error.

That would allow arbitrarily complex expressions to resolve to "default", but not perform any operations on it - a bit like propagating sqrt(-1) through an engineering formula where you know it will be cancelled out eventually.

I 100% agree with this.
"default" should not evaluate to a value before sending it as an
argument to the function or method.
I understand from what the RFC author wrote a while ago that doing so
(evaluating to the actual default value using reflection) was the
easier and perhaps only viable way at the moment, but if that's the
case, I don't think the value of this RFC justifies doing something
like that, which to me seems like a hack.

For those already expressing interest in being able to modify a binary
flag default parameter using this "trick", I still don't think it
justifies this RFC. In my opinion, functions that accept multiple
arbitrary options by compressing them into a binary flag have a badly
designed interface to begin with.

So, my 10c: Make "default" a pure keyword / immutable value if
possible, or reconsider whether this feature is really worth the fuss.

The problem here is that `foo(default | ADDITIONAL_FLAG)` is so far the most compelling use case I've seen for this feature. It's really one of only two I see myself possibly using, frankly. So a limitation that would disallow that pattern would render the RFC almost useless.

The other compelling case would be the rare occasions where today you'd do something like this:

if ($user_provided_value) {
  foo($beep, $user_provided_value);
} else {
  foo($beep);
}

Which this RFC would allow to be collapsed into

foo($beep, $user_provided_value ?: default);

So far those are the two use cases I can realistically see being helpful, and I acknowledge both are. I recognize that "limiting the allowed expression structures arbitrarily is way harder than it sounds" is a valid argument as well. At the same time, John C has offered some valid examples of cases where it would open up additional footguns, and we want to minimize those in general. Those shouldn't be ignored, either.

At the moment I'm on the fence on this RFC. I could go either way right now, so I'm watching to see how it develops.

--Larry Garfield

Hey Jordi,

On 26.8.2024 13:55:52, Jordi Boggiano wrote:

One question (sorry if someone already asked, I scanned the thread but it is getting long..):

Taking this example from the RFC:

function g\($p = null\) \{
    f\($p ?? default\);
\}

Could you go one step further and use default by default but still allow null to be passed in?

function g\($p = default\) \{
    f\($p\);
\}

I suppose this would mean $p has to hold this "default" value until a function call is reached, at which point it would resolve to whatever the default is. This probably complicates things for very little gain but I had to ask.

First, it would be some sort of spooky action at a distance, likely add a new zval type etc.; lots of special handling for a likely minor benefit.

Second, I'd expect that bit of syntax do be useful in inheritance - like you implement or override a parent class/interface method specifying a default; then you can just use the default of the parent method.

Bob

From the example you gave it appears that we can have a concrete problem when:

  1. There is a parameter with a default value,
  2. That parameter is type-hinted,
  3. The hinted type is declared as a union type,
  4. An earlier version of the library initialized the default with a value having one of the union types,
  5. End-user developers used the library and then use default as an expression of that type, and finally
  6. The library developer changed the initialization of the default to a different type from the union.

Did I correctly identify the problematic use-case?

Not really. #2 and #3 are irrelevant mixed is actually much more problematic, I wanted to provide an example that was strongly typed intentionally to show the problem even when types were explicit. The relevant portion is #1, #5 and #6.

Ok, so for argument sake, what if they revise the RFC to only allow default to be used in an expression when the parameter is not type-hinted with a union? Would that address your concern? Or are there other use-cases that are problematic that do not hinge on the parameter being type-hinted as a union type?

It wouldn’t be enough, offhand it’d also have to be forbidden for mixed (at which point I think the utility isn’t there anymore).

On Mon, Aug 26, 2024, 12:02 PM Larry Garfield <larry@garfieldtech.com> wrote:

On Mon, Aug 26, 2024, at 6:36 AM, Jakob Givoni wrote:

On Mon, Aug 26, 2024 at 12:49 PM Rowan Tommins [IMSoP]
<imsop.php@rwec.co.uk> wrote:

On Mon, 26 Aug 2024, at 10:14, Bilge wrote:

You’re absolutely right, I would be interested to see any viable patch
that effectively implements a set of restrictions on how default may
be used. Requesting it be done at the parser level was not meant as a
gotcha, that’s just how I (with my lack of experience) would have
approached it, but certainly trapping cases in the compiler is equally,
if not more valid and/or practical.

Another approach that occurred to me was in the executor: rather than evaluating to the default value immediately, “default” could resolve to a special value, essentially wrapping the reflection parameter info. Then when the function is actually called, it would be “unboxed” and the actual value fetched, but use in any other context would be a type error.

That would allow arbitrarily complex expressions to resolve to “default”, but not perform any operations on it - a bit like propagating sqrt(-1) through an engineering formula where you know it will be cancelled out eventually.

I 100% agree with this.
“default” should not evaluate to a value before sending it as an
argument to the function or method.
I understand from what the RFC author wrote a while ago that doing so
(evaluating to the actual default value using reflection) was the
easier and perhaps only viable way at the moment, but if that’s the
case, I don’t think the value of this RFC justifies doing something
like that, which to me seems like a hack.

For those already expressing interest in being able to modify a binary
flag default parameter using this “trick”, I still don’t think it
justifies this RFC. In my opinion, functions that accept multiple
arbitrary options by compressing them into a binary flag have a badly
designed interface to begin with.

So, my 10c: Make “default” a pure keyword / immutable value if
possible, or reconsider whether this feature is really worth the fuss.

The problem here is that foo(default | ADDITIONAL_FLAG) is so far the most compelling use case I’ve seen for this feature. It’s really one of only two I see myself possibly using, frankly. So a limitation that would disallow that pattern would render the RFC almost useless.

The other compelling case would be the rare occasions where today you’d do something like this:

if ($user_provided_value) {
foo($beep, $user_provided_value);
} else {
foo($beep);
}

Which this RFC would allow to be collapsed into

foo($beep, $user_provided_value ?: default);

So far those are the two use cases I can realistically see being helpful, and I acknowledge both are.

I can see a few others:

  • string concatenation. I might want to prepend or append a string to a default.

  • fractional or multiplicative application, e.g. for durations/timeouts. These might require testing for non-zero first as well.

  • decorating a default instance (e.g. to lazily create a proxy without knowing the default implementation used for an argument hinted against an interface)

And these are just off the top of my head. I could likely identify more with a short bit of time looking through some libraries I regularly use.

I recognize that “limiting the allowed expression structures arbitrarily is way harder than it sounds” is a valid argument as well. At the same time, John C has offered some valid examples of cases where it would open up additional footguns, and we want to minimize those in general. Those shouldn’t be ignored, either.

IF it’s possible to accomplish, I think it’s better to identify the “leaving this open will create WTF situations” than to prematurely lock everything down up front.

There’s been a few good lists about the cool things this could enable, demonstrating the value; maybe now we should focus on the “we absolutely shouldn’t enable” pieces to allow for broader consensus.

At the moment I’m on the fence on this RFC. I could go either way right now, so I’m watching to see how it develops.

–Larry Garfield

This seems like a valid and balanced position from Larry.

···

On 26/08/2024 19:11, Matthew Weier O’Phinney wrote:

On Mon, Aug 26, 2024, 12:02 PM Larry Garfield <larry@garfieldtech.com> wrote:

I recognize that “limiting the allowed expression structures arbitrarily is way harder than it sounds” is a valid argument as well. At the same time, John C has offered some valid examples of cases where it would open up additional footguns, and we want to minimize those in general. Those shouldn’t be ignored, either.

IF it’s possible to accomplish, I think it’s better to identify the “leaving this open will create WTF situations” than to prematurely lock everything down up front.

There’s been a few good lists about the cool things this could enable, demonstrating the value; maybe now we should focus on the “we absolutely shouldn’t enable” pieces to allow for broader consensus.

I like this approach. I’m still not sure I’d want to pursue adding exclusions, but if we can identify something that’s obviously bad and/or dangerous then we can consider that short list for exclusion. That is much more compelling than starting out by banning everything and arbitrarily whitelisting those things someone personally has a use for.

Cheers,
Bilge

On Aug 26 2024, at 2:39 pm, Bilge bilge@scriptfusion.com wrote:

I like this approach. I’m still not sure I’d want to pursue adding exclusions, but if we can identify something that’s obviously bad and/or dangerous then we can consider that short list for exclusion. That is much more compelling than starting out by banning everything and arbitrarily whitelisting those things someone personally has a use for.

Perhaps the answer could be to only allow the use of default when the assigned default value is a scalar value – no objects, arrays, enums, etc (and no mixed )… It seems like a compromise that accomplishes a healthy portion of the stated use-cases while avoiding many of the foot-guns scenarios.

Coogle
(PS - I’m going to start signing off with my old-skool nickname around here, feel free to reference me using it to disambiguate since there are multiple Johns)

Hi :slight_smile: Don’t worry, you’re right, this is an important topic that I was still finalising in the past 48 hours and is still an omission from the RFC, and as such we haven’t discussed it on the list yet.

···

On 26/08/2024 15:20, Stephen Reay wrote:

Hi,

I haven’t followed the entire thread in depth so I apologise if this was already answered, but I haven’t noticed it being mentioned/clarified yet.

Can you clarify in the following, is the arm comparing against match’s default or the parameter’s default? Or to put it another way, in the second call, If $arg is 2, will the match error out due to an unmatched subject, or will it pass 1 to F?

function F(int $foo = 1) {}

F(match(default) { default => default });



```
F(match($arg) { 'a' => 0, default => default });
```

Thank you for your (excellent) question. The answer is it will pass 1, and the reason is as follows.

F(match(default) { default => default }); is interpreted as match (default expression) { default arm => default expression }, therefore the first and last defaults will be substituted with the argument’s default, but not the middle one. However, that is only the case when the default arm is written exactly as default.

You can turn the condition into an expression, in which case all three will act as expressions and be substituted accordingly, e.g. F(match(default) { (int) default => default });.

Since the default condition is now an expression, you can still have a default arm in addition to this, e.g. the following would be valid:

F(match(default) {
(int) default => default,
default => default,
});

Whilst this is a curiosity, consider that passing match expressions directly to arguments is something I personally have never witnessed and that goes doubly for combining it with default. So, whilst it is interesting to know, and important for the RFC to state the specific semantics of this scenario, the practical applications are presumed slim to none.

Cheers,
Bilge

On Aug 26, 2024, at 8:28 AM, Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> wrote:

On Mon, 26 Aug 2024, at 11:43, Mike Schinkel wrote:

You ask how a library can provide access to that default, and the answer is generally pretty trivial: define a public constant, and refer to it in the parameter definition.

A global? Really?

I didn’t say “global”, I said “public”.

I was imprecise in my description. I meant “global” in the general programming sense and not in the PHP keyword global sense.

Sorry about my lack of precision here.

Since you’re keen on real-world examples, here’s a simplified version of a real class:

class ConfigSource
{
private HttpClientInterface $httpClient;

public const DEFAULT_CONSUL_URL = ‘http://localhost:8500’;

public function __construct(private string $consulUrl=self::DEFAULT_CONSUL_URL, ?HttpClientInterface $httpClient=null)
{
if ( $httpClient === null ) {
$httpClient = new HttpClient();
$httpClient->setRequestTimeoutSecs(5);
$httpClient->setConnectRequestTimeoutSecs(5);
}
$this->httpClient = $httpClient;
}
}

This constructor has two optional parameters; one of them uses a default which is also referenced explicitly in another class, so is exposed as a constant; the other uses a completely opaque method of creating a default object, which even Reflection could not expose.

The caller doesn’t need to know how any of this works to make use of the class. The contract is “__construct(optional string $consulUrl, optional ?HttpClientInterface $httpClient)”

The purpose of the optional parameters is so that you can write “$configSource = new ConfigSource();” and trust the library to provide you a sensible default behaviour.

Thank you (sincerely) for taking the time to author a real-world use-case.

If it was decided that the code for creating a default HttpClient was needed elsewhere, it could be refactored into a method, with appropriate access:

public function __construct(private string $consulUrl=self::DEFAULT_CONSUL_URL, ?HttpClientInterface $httpClient=null)
{
$this->httpClient = $httpClient ?? $this->getDefaultHttpClient();
}

public function getDefaultHttpClient(): HttpClient
{
$httpClient = new HttpClient();
$httpClient->setRequestTimeoutSecs(5);
$httpClient->setConnectRequestTimeoutSecs(5);
return $httpClient;
}

So, nullable is an equivalent to the union-type concern my discussion with John Coggeshall uncovered, correct?

When two different types can be passed then we cannot depend on the default being a specific type.

Or perhaps the HttpClient becomes nullable internally:

public function __construct(private string $consulUrl=self::DEFAULT_CONSUL_URL, private ?HttpClientInterface $httpClient=null) {}

Or maybe we allow explicit nulls, but default to a simple class instantiation:

public function __construct(private string $consulUrl=self::DEFAULT_CONSUL_URL, private ?HttpClientInterface $httpClient=new HttpClient) {}

None of these are, currently, breaking changes - the contract remains “__construct(optional string $consulUrl, optional ?HttpClientInterface $httpClient)”.

Great example.

Now consider the following, assuming the RFC passed but with an added requirement that match() be used exhaustively for nullable parameters (shown) or parameters type hinted with unions (not shown):

$configSource = new ConfigSource(default,match(default){
NULL=>new ConfigSource->getDefaultHttpClient()->withRequestTimeoutSecs(5),
default=>default->withRequestTimeoutSecs(5)
})

Ignoring that this expression is overly complex, if covering all “types” would be a requirement in the RFC — where NULL is a type here for purposes of this analysis —can you identify another breaking change if the RFC passed?

On Aug 26, 2024, at 8:43 AM, Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> wrote:
On Mon, 26 Aug 2024, at 11:43, Mike Schinkel wrote:

But saying that we can be certain default values are private and we
want to keep them that way is provably false by the existence of
Reflection.

Right now, accessing the default value of an optional parameter is in the “possible if you mess around with Reflection APIs” set; maybe we do want to move it to the “is part of the language” set, maybe we don’t; but claiming there is no distinction is nonsense.

Now don’t go putting words in my mouth, Rowan. :wink:

I did not say there was no distinction, I said that we cannot be certain the values are indeed private. Subtle difference I admit, but there is a difference. :slight_smile:

On Aug 26, 2024, at 12:59 PM, Larry Garfield <larry@garfieldtech.com> wrote:
The problem here is that foo(default | ADDITIONAL_FLAG) is so far the most compelling use case I’ve seen for this feature. It’s really one of only two I see myself possibly using, frankly…

The other compelling case would be the rare occasions where today you’d do something like this:

foo($beep, $user_provided_value ?: default);

Is foo((default)->WithLogger(new Logger)); not a compelling use-case — which illustrates the clone of an default initialized object with injected dependencies with one or a few properties then modified — or did you just miss that use-case in the volume of this thread?

On Aug 26, 2024, at 1:35 PM, John Coggeshall <john@coggeshall.org> wrote:

From the example you gave it appears that we can have a concrete problem when:

  1. There is a parameter with a default value,
  2. That parameter is type-hinted,
  3. The hinted type is declared as a union type,
  4. An earlier version of the library initialized the default with a value having one of the union types,
  5. End-user developers used the library and then use default as an expression of that type, and finally
  6. The library developer changed the initialization of the default to a different type from the union.
    Did I correctly identify the problematic use-case?

Not really. #2 and #3 are irrelevant mixed is actually much more problematic, I wanted to provide an example that was strongly typed intentionally to show the problem even when types were explicit. The relevant portion is #1, #5 and #6.

For the purpose of this analysis mixed can be considered just a special case of a union-type where the types available are “all.”

You say #2 and #3 are irrelevant and mention mixed, but if we consider mixed as a special type of union then #2 and #3 become relevant again (as well as nullable, as Rowan uncovered, which is itself a special type of union.)

Or maybe there are problems with single types that we have not uncovered yet?

Ok, so for argument sake, what if they revise the RFC to only allow default to be used in an expression when the parameter is not type-hinted with a union? Would that address your concern? Or are there other use-cases that are problematic that do not hinge on the parameter being type-hinted as a union type?

It wouldn’t be enough, offhand it’d also have to be forbidden for mixed

Okay — with the comment below considered — then assuming the RFC were to disallow using default as an expression when the parameter were type-hinted with mixed (and when no type hint is used), what are breaking changes we still need to worry about?

Note default could still be used with mixed, but only by itself, not in an expression.

(at which point I think the utility isn’t there anymore).

I think this argument is provably false by the fact that — minimally — the use-case that has resonated with several people who are otherwise lukewarm has been bitmapped flags, for which type hinting as mixed would be an obvious code smell.

But even so, it is up to the RFC author to decide if those limitations would diminish the utility too much to continue the RFC, and then for the voters to decide if they agreed with the remaining utility.

On Aug 26, 2024, at 2:11 PM, Matthew Weier O’Phinney <mweierophinney@gmail.com> wrote:

There’s been a few good lists about the cool things this could enable, demonstrating the value; maybe now we should focus on the “we absolutely shouldn’t enable” pieces to allow for broader consensus.

+1!

On Aug 26, 2024, at 2:49 PM, John Coggeshall <john@coggeshall.org> wrote:

Perhaps the answer could be to only allow the use of default when the assigned default value is a scalar value – no objects, arrays, enums, etc (and no mixed )…

FWIW, that would eliminate many of the use-cases for me where I see the feature has value.

On Aug 26, 2024, at 3:12 PM, Bilge <bilge@scriptfusion.com> wrote:

Whilst this is a curiosity, consider that passing match expressions directly to arguments is something I personally have never witnessed and that goes doubly for combining it with default. So, whilst it is interesting to know, and important for the RFC to state the specific semantics of this scenario, the practical applications are presumed slim to none

Eh, see nullable and union types (mentioned above.) :slight_smile:

-Mike

P.S. The more I think about this, the more I think that the default value should be a formal part of the function signature. The fact that has not been explicitly defined before now — due to the fact it wasn’t relevant before this RFC — does not automatically require that it not be a formal part of the function signature, that is just the implicit status quo. This is likely something this RFC or a precursor RFC should ask voters to vote on explicitly, and then it would be decided.

On 26 August 2024 20:27:59 BST, Mike Schinkel <mike@newclarity.net> wrote:

So, nullable is an equivalent to the union-type concern my discussion with John Coggeshall uncovered, correct?

It's not really anything to do with nulls, or unions. It's somewhat related to "contravariance of input": that it should always be safe to substitute a function that accepts a wider range of input. If you have an input "DateTime $d", it's always safe to substitute (in a subclass, or a later version) "DateTimeInterface $d", or "?DateTime $d", or "DateTime|string $d", or any combination of the above.

And right now, while doing so, you can safely change the default value to any value that is valid for the new input type, because all the caller knows is that the parameter is optional.

For instance, "DateTime $d=new DateTime('now')" might become "DateTimeInterface $d=new DateTimeImmutable('now')". Or maybe one subclass has "DateTimeInterface $d=new DateTimeImmutable('now')" and another has "DateTimeInterface $d=new DateTime('now')".

Now consider the following, assuming the RFC passed but with an added requirement that `match()` be used exhaustively for nullable parameters (shown) or parameters type hinted with unions (not shown):

$configSource = new ConfigSource(default,match(default){
NULL=>new ConfigSource->getDefaultHttpClient()->withRequestTimeoutSecs(5),
default=>default->withRequestTimeoutSecs(5)
})

Ignoring that this expression is overly complex, if covering all "types" would be a requirement in the RFC — where `NULL` is a type here for purposes of this analysis —can you identify another breaking change if the RFC passed?

Certainly. A new version of the library can change the parameter to "?NetworkClientInterface $httpClient=new WebSovketClient". (The name kept the same because named parameters mean callers may be relying on it.)

As written, it's also entirely pointless, because you've called getDefaultClient(), whose entire purpose is to be a stable public API which you can rely on for that purpose, rather than peeking into implementation details.

I did not say there was _no_ distinction, I said that we cannot be *certain* the values are indeed private. Subtle difference I admit, but there _is_ a difference. :slight_smile:

This is true only in the extremely pedantic sense that "we can't be certain that private properties are private". It's not at all relevant to my argument, which is that right now, the language treats default values as part of the implementation, not part of the public API.

P.S. The more I think about this, the more I think that the default value *should* be a formal part of the function signature. The fact that has not been explicitly defined before now — due to the fact it wasn't relevant before this RFC — does not automatically require that it not be a formal part of the function signature, that is just the implicit status quo. This is likely something this RFC or a precursor RFC should ask voters to vote on explicitly, and then it would be decided.

Well, that would get an immediate "no" from me. I see absolutely no reason to restrict a function's choice of default beyond being valid for the declared type.

Rowan Tommins
[IMSoP]