[PHP-DEV] [RFC] Default expression

On 25 August 2024 21:00:03 BST, Bilge <bilge@scriptfusion.com> wrote:

class Suspension {
/**
* @param int $delay Specifies the delay in milliseconds.
*/
public function suspend(int $delay = 1_000) {
var_dump($delay);
}
}

class MySuspension extends Suspension {
/**
* @param float|int|null $delay Specifies the delay in seconds.
*/
public function suspend(float|int|null $delay = null) {
parent::suspend((int)(($delay ?? 0) * 1000) ?: default);
}
}

new MySuspension()->suspend(2.2345); // int(2234)

Not only have I demonstrated the need to use multiplication or division to change the scale, but also the need to cast.

Possibly something got lost as you redrafted the example, because as you've written it, neither the multiplication nor the cast are applied to the value looked up by "default". The parameter reduces to "(expression) ?: default", which I've already agreed is useful.

I was thinking about why "bitwise or" feels so different from other operators here, and I realised it's because it's idempotent (I hope I'm using that term correctly): if the specified bits are already set, it will have no effect.

Consequently, we know that ($x | SOME_FLAG) & SOME_FLAG === SOME_FLAG without knowing the value of $x. That in turn means that regardless of how the default value changes in future, we know what "default | JSON_PRETTY_PRINT" will do.

It's as though each bit flag is a separate parameter, and you're saying "pass true to $prettyPrint, but let the implementation decide sensible defaults for all other flags".

The majority of operators don't have that property, so they require some additional assumptions about the default, which might not hold in future. For instance, if you use "default + 1", you are implicitly assuming that the default value is not the maximum allowed value.

Rowan Tommins
[IMSoP]

On 25 August 2024 21:29:45 BST, John Bafford <jbafford@zort.net> wrote:

This is only by current convention. It used to be that parameter names were not part of the API contract, but now with named parameters, they are.

Indeed, and it remains highly controversial among library authors, and is even used as a justification for this RFC.

There's no reason default values couldn't (or shouldn't) become part of the API contract in the same way.

I agree that they *could*, but I am making the case that they *should not*. The ability to specify an optional parameter which is subject to change is a very useful one, frequently used. If a library author *wants* to expose the default value for manipulation by users, they can make it available as a constant, as with e.g. PASSWORD_DEFAULT.

I can definitely see the use in features that enhance the existing use of default parameters to mean "I trust the implementation to do the right thing, and am not interested in specifying this parameter". (And see my earlier post on how bit flags can still fit it into this meaning.)

None of the examples so far have persuaded me that there is sufficient value in extending that to "every optional parameter also acts a public constant that the caller can read out and act on at will".

Rowan Tommins
[IMSoP]

On 25/08/2024 22:09, Rowan Tommins [IMSoP] wrote:

On 25 August 2024 21:00:03 BST, Bilge<bilge@scriptfusion.com> wrote:

class Suspension {
/**
* @param int $delay Specifies the delay in milliseconds.
*/
public function suspend(int $delay = 1_000) {
var_dump($delay);
}
}

class MySuspension extends Suspension {
/**
* @param float|int|null $delay Specifies the delay in seconds.
*/
public function suspend(float|int|null $delay = null) {
parent::suspend((int)(($delay ?? 0) * 1000) ?: default);
}
}

new MySuspension()->suspend(2.2345); // int(2234)

Not only have I demonstrated the need to use multiplication or division to change the scale, but also the need to cast.

Possibly something got lost as you redrafted the example, because as you've written it, neither the multiplication nor the cast are applied to the value looked up by "default". The parameter reduces to "(expression) ?: default", which I've already agreed is useful.

Great! I'm glad we're finally getting to this, because I think this is what you, and everyone advocating for a restricted grammar, is actually missing. You think you've caught me in some kind of "gotcha" moment, but fair warning, I'm about to play my Uno Reverse card.

What you're saying is that, somehow, even though the default must be constrained to a restricted subset of permissible grammars, it is still acceptable to have unrestricted expressions in the other operands. So, in this example, somehow `expr ?: default` is OK.This is simply impossible and would cause catastrophic shift/reduce conflicts in the grammar. If `default` is to live in a restricted subset of allowed expression grammars, then it can only recurse with those same restrictions, meaning /both/ operands of any operators are so restricted. Ergo I do not need to demonstrate the usefulness of applying other operators /directly/ to `default`, merely including them /somewhere/ in the expression is sufficient to demonstrate they are useful because at that point we're back to recursing the general expression grammar (free of any restrictions), unless and until you're willing to concede those particular operators I've just demonstrated the useful application for should be entered into the arbitrarily-selected restricted subset of grammars allowed to apply to `default`.

If you believe I am incorrect about this, I encourage you to submit a (working) Bison patch to demonstrate how a restricted expression grammar subset can still recurse with the unrestricted superset, then we can start having this discussion more seriously.

Cheers, Bilge

On 25 August 2024 22:51:45 BST, Bilge <bilge@scriptfusion.com> wrote:

Great! I'm glad we're finally getting to this, because I think this is what you, and everyone advocating for a restricted grammar, is actually missing. You think you've caught me in some kind of "gotcha" moment, but fair warning, I'm about to play my Uno Reverse card.

You could have got to it much quicker by just saying it earlier, particularly when explaining how the current implementation is *not* the easy path.

I was not in the slightest thinking I'd caught any kind of "gotcha", I was repeating something I'd already said multiple times, that the *behaviour* I feel is justified is having "default" usable in the RHS of a ternary or coalesce.

I'm not an expert on parsers, and never claimed to be, so it's not particularly surprising to me that I've overlooked a reason why "expr ?: default" can't be included without also including "default ?: expr", and will just have to take your word for it.

It doesn't, unfortunately, persuade me that the behaviour proposed is sensible.

Rowan Tommins
[IMSoP]

On 25/08/2024 23:31, Rowan Tommins [IMSoP] wrote:

It doesn't, unfortunately, persuade me that the behaviour proposed is sensible.

It should. But since it has apparently failed in that regard, I suggest you take me up on my challenge to implement the grammar you want with a patch and you will quickly convince yourself one way or the other. The truth doesn't exist in my head or yours, nor on this mailing list. The truth always lies in the code, which is why RFC authors are strongly encouraged to pursue patches where there is doubt, and similarly, I think counter-proposals on the mailing list should follow suit, otherwise we can find ourselves arguing over nothing.

Kind regards, Bilge

On 26.8.2024 00:31:57, Rowan Tommins [IMSoP] wrote:

I'm not an expert on parsers, and never claimed to be, so it's not particularly surprising to me that I've overlooked a reason why "expr ?: default" can't be included without also including "default ?: expr", and will just have to take your word for it.

It doesn't, unfortunately, persuade me that the behaviour proposed is sensible.

Rowan Tommins
[IMSoP]

Hey Rowan,

just to state this:

It is almost never sensible to arbitrarily restrict grammars.

In the sense of "allow this expression just in a context of this given list of expressions". Sure, in some cases the permitted grammar doesn't make sense (like, why would we allow arithmetic operators on the left-hand side of a coalesce operation "($a + $b) ?? $c"), but that's on the user to write a minimal bit of sensible code.

I hope you can understand that; thanks,

Bob

On 25 August 2024 23:42:20 BST, Bilge <bilge@scriptfusion.com> wrote:

On 25/08/2024 23:31, Rowan Tommins [IMSoP] wrote:

It doesn't, unfortunately, persuade me that the behaviour proposed is sensible.

It should. But since it has apparently failed in that regard, I suggest you take me up on my challenge to implement the grammar you want with a patch and you will quickly convince yourself one way or the other.

I think I have been perfectly consistent in saying that I am discussing the proposed language behaviour, not anything about how it could or should be implemented.

If it's a case of "unfortunately, doing the right thing is impossible, so we're proposing this compromise", then that's a reasonable position, but not how this has been presented. I also think it is perfectly reasonable to conclude that the compromise gives away too much.

In particular, I think allowing assignments and method calls to "read out" a value which was previously a private implementation detail accessible only through the Reflection API, is a significant language change with a net negative impact. If that's the required tradeoff to allow "(some expression) ?: default", then my position is we should do without it.

Regards,
Rowan Tommins
[IMSoP]

On Aug 25 2024, at 5:51 pm, Bilge bilge@scriptfusion.com wrote:

If you believe I am incorrect about this, I encourage you to submit a (working) Bison patch to demonstrate how a restricted expression grammar subset can still recurse with the unrestricted superset, then we can start having this discussion more seriously.

I don’t think the restrictions being championed by Rowan (to which I concur) wouldn’t be solved in the parser at compile time anyway – Enforcement would have to happen in the VM at runtime during execution.

That’s not really how that works – I mean it’s not really up to anyone else to write a PR to implement their version of your RFC just because they disagree with (portions of) the concept.

···

On 25/08/2024 23:31, Rowan Tommins [IMSoP] wrote:

`

It doesn't, unfortunately, persuade me that the behaviour proposed is sensible.

`

It should. But since it has apparently failed in that regard, I suggest you take me up on my challenge to implement the grammar you want with a patch and you will quickly convince yourself one way or the other. The truth doesn’t exist in my head or yours, nor on this mailing list. The truth always lies in the code, which is why RFC authors are strongly encouraged to pursue patches where there is doubt, and similarly, I think counter-proposals on the mailing list should follow suit, otherwise we can find ourselves arguing over nothing.

This is a general comment so not replying to anyone in particular hence the top-posting (but with some bottom-posts in particular reply below.)

The RFC — which I am neither pro nor con for — proposes allowing default to be an expression, and while many like and support the RFC others are (not quite) demanding that it must be limited to its own subset of expressions, because, concerns.

It seems to me the "concerns" fall into two categories:

1. Some expressions make no sense so we should disallow them, and
2. Allowing default to be an expression makes it part of the published API.

Speaking only to #1 for the moment, there are many different places in PHP where certain expressions make no sense either, yet I do not see those objecting calling the question for those other scenarios, and I ask myself "Why not?"

One obvious answer is "because those are status quo and this is not" but I also maybe because the many non-sensical things that can be done elsewhere are, in practice, not a problem as nobody ever does them. And why not? Because...they are nonsensical. Given this the arguments against feel to me to be rather bikesheddy. But I will come back to #1.

Moving on to #2 I respect the argument in theory. But as we are constantly told the RFC author's burden is to prove the necessity, it seems only reasonable and fair to expect that — when many people see the utility of and support an RFC — that those who object to it should provide some concrete examples of how their concerns would manifest problems rather than just say "it might allow something bad."

I have looked for but yet not seen any examples of how effectively publishing a default value could actually cause a significant problem — and this is the important part — in a real-world scenario and not just a contrived one.

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?

Given how much support this RFC appears to have, I think it is incumbent on those against it to give valid real-world examples where their fears would materialize and cause more than a trivial problem. Again, while I am not pro or con on this RFC I do think the pro arguments have been better conceived than the con arguments, regardless of the underlying merit of the RFC, hence why I comment.

On Aug 25, 2024, at 4:40 PM, Larry Garfield <larry@garfieldtech.com> wrote:
To the extent possible, the language and compiler should prevent you from doing stupid things, or at least make doing stupid things harder.
...
This is the design philosophy behind all type systems: Make illogical or dangerous or "we know it can't work" code paths a compile error, or even impossible to express at all.

Good design makes the easy path the safe path.

In concept, I am in violent agreement with you.

Rob has shown some possible, hypothetical uses for some of the seemingly silly possible combinations, which may or may not carry weight with people. But there are others that are still unjustified, so for now, I would still put "default != 5" into the "stupid things" category, for example.

I think you imply that there is a dichotomy or at least a 1-dimensional spectrum?

I think however there is at least a two (2) other dimensions, and they include:

2. "How trivial vs. damaging is a thing" and
3. "How likely are people to accidentally do that stupid thing?"

If the damage is trivial and/or the thing they is very unlikely for them to do — maybe because it is non-sensical — then do we really need extra guard rails? Passing `default != 5` to an int parameter is (IMO) both unlikely and not particularly damaging (if type-hinted, an error will be thrown.)

But if you disallow `default!=5`, then you (likely?) also disallow `default!=5 ? default : 0` and any other *sensical* expression with that sub-expression. Do we really want to flesh out all the potential nonsensical expressions for a given use-case and build a custom parser for this one RFC?

I could see having a context-based expression parser getting an RFC in its own right, but IMO doing so would be wildly out-of-scope for this RFC. And if such as RFC were pursued, I think in the vast majority of cases it should be employed by the userland developer and not in PHP core. But I digress.

So please provide examples where nonsensical expressions cause real-world problems. If you can, then maybe everyone supportive of the RFC will see use-cases where the RFC is problematic. But without good examples illustrating serious problems, is there really anything to worry about here?

BTW, you argue about unintended consequences that could not be foreseen hence why complex features need to be fully fleshed out — and I agree — but this RFC does not appear to be complex so finding problematic expressions should be relatively easy, if those examples do indeed exist.

On Aug 25, 2024, at 4:32 PM, John Coggeshall <john@coggeshall.org> wrote:
but I just can't get behind the idea that (default)->foobar() is a valid expression in this context or a good idea for the language.

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));

On Aug 25, 2024, at 12:23 PM, John Coggeshall <john@coggeshall.org> wrote:
I won't vote for this RFC if the above code is valid, FWIW. Unlike include , default is a special-case with a very specific purpose -- one that is reaching into someone else's API in a way the developer of that library doesn't explicitly permit.

Ok, so if a developer of the API currently wants to indicate that other developers CAN explicitly reach in then how would they do that, currently?

Your argument seems to be it should implicitly be disallowed, but I do not see any way in which you are proposing a developer could explicitly empower a developer to allow it. At least not without a ton of boilerplate which, for each default value, turns into a lot of extra code to create and then maintain.

It seems one-sided to argue against allowing something that "has not been explicitly allowed" if there is no way to explicitly allow it. And I get you may say "well that's not my job" to which —if you did — I would ask "Do you really only want to be the one who brings the problem but not the solution?"

On Aug 25, 2024, at 12:21 PM, Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> wrote:
The Reflection API is a bit like the Advanced Settings panel in a piece of software, it comes with a big "Proceed with Caution" warning. You only move something from that Advanced Settings panel to the main UI when it's going to be commonly used, and generally safe to use. I don't think allowing arbitrary operations on a value that's declared as the default of some other function passes that test.

You analogy is faulty. You are conflating a complex API — Reflection — with one use-case of that API which — per the RFC — has well-defined syntax and semantics and purely due to its simplicity is more likely to be used than the Reflection API and far less likely to be used incorrectly than the Reflection API.

On Aug 25, 2024, at 11:31 AM, Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> wrote:
A few rules that seem logical to me:

All those rules were "these are the things we should not do" but few "here is why these might be a problem" but no examples, and no "here is why developers are *likely* to do those things that are actually problematic, with examples."

Or said more colloquially, "If a language has a foot-gun but no developer ever pulls that foot-gun's trigger, is it really a foot-gun?"

library authors ...now also need to question whether someone is relying on "default + 1" having some specific effect?

Can you give a specific example from code we are likely to find in a production application — vs. a forum debate — where someone is likely to use `default+1` AND where it would be problematic for the author?

There may well be one but I cannot currently envision it, which is why I ask.

Beyond that, I'm struggling to think of meaningful uses: "whatever the function sets as its default, do the opposite"; "whatever number the function sets as default, raise it to the power of 3"; etc. Again, they can easily be added in later versions, if a use case is pointed out.

Being pedantic, but how is `default^3` the "opposite" of `default`?

On Aug 25, 2024, at 4:29 PM, John Bafford <jbafford@zort.net> wrote:
is because PHP doesn't currently allow default values to be computed at runtime. (Maybe it should.)

(Amen.)

Summary:

After writing this response I am currently leaning into the pro column for this RFC because I think the arguments for the pro are stronger than the arguments for the con. But if better con arguments emerge my opinion could change.

Where I would see this functionality to be most useful would be:

1. Modifying a default bitmapped argument to add or remove one or more bits.

2. Creating a class instance based on the default — with Wither methods() — where I need to modify a few properties but want to keep all the rest of the default value.

Not saying there are not other use-cases, but those other use-cases have not (yet?) occurred to me.

-Mike

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

On Aug 26 2024, at 12:44 am, Mike Schinkel mike@newclarity.net wrote:

Speaking only to #1 for the moment, there are many different places in PHP where certain expressions make no sense either, yet I do not see those objecting calling the question for those other scenarios, and I ask myself “Why not?”

One obvious answer is “because those are status quo and this is not” but I also maybe because the many non-sensical things that can be done elsewhere are, in practice, not a problem as nobody ever does them. And why not? Because…they are nonsensical. Given this the arguments against feel to me to be rather bikesheddy. But I will come back to #1.

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.

Moving on to #2 I respect the argument in theory. But as we are constantly told the RFC author’s burden is to prove the necessity, it seems only reasonable and fair to expect that — when many people see the utility of and support an RFC — that those who object to it should provide some concrete examples of how their concerns would manifest problems rather than just say “it might allow something bad.”

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 have looked for but yet not seen any examples of how effectively publishing a default value could actually cause a significant problem — and this is the important part — in a real-world scenario and not just a contrived one.

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? That is the problem. It’s that some library developer decided to change the default value for their own purposes and now any code that was relying on that default value has broken. I don’t think anyone needs to go hunting around GitHub or something to prove that people change default values between library releases, nor that this RFC would introduce a new dependency between upstream APIs and the code that uses them to manage. See below for a fairly reasonable example I whipped up.

  1. “How likely are people to accidentally do that stupid thing?”

Experience has shown us time and time again that people will use the features that get implemented, and once they exist we are stuck with the consequences. Developers who consume APIs are rarely worrying about the developers who wrote those APIs. This is making it not only very easy to do a stupid thing, it’s making the fact people are doing a stupid thing the problem of an entirely different project/developer who now has to deal with the reality any time they touch a default value they have to worry about the downstream impacts, being granted zero control to prevent it from happening. It’s also something that would be hard for downstream developers to even realize was a problem…

Library dev: “BC break, I changed this default value”
Downstream dev: “Okay… so how do I fix this code someone else wrote 2 years ago so I can upgrade?”
Library dev: “I dunno, depends on how you used default so you’re on your own. I guess you better grep and hope you catch them all”

But if you disallow default!=5, then you (likely?) also disallow default!=5 ? default : 0 and any other sensical expression with that sub-expression. Do we really want to flesh out all the potential nonsensical expressions for a given use-case and build a custom parser for this one RFC?

As I previously said, I don’t think this would be addressed in the parser – I think it’d have to be a runtime check (if such a check were to exist).

That aside, no I don’t really want to flesh out all the potential expressions to build those rules into the engine. I believe if we are going to allow expressions for default values as proposed we need to build those rules, but given the circumstances I think it’s more likely the RFC needs to be reconsidered, updated to reflect feedback, or withdrawn.

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:

<?php enum LoggerType: string { case DB = 'db'; case FILE = 'file'; case CLOUD = 'cloud'; public function getLogger(): LoggerInterface { return match($this) { static::DB => new DatabaseLogger(), static::FILE => new FileLogger(), static::CLOUD => new CloudLogger() }; } } interface LoggerInterface { public function log(string $x); } // Assume DatabaseLogger, etc. exist and implement `LoggerInterface` class A { protected ?LoggerInterface $log; public function withLogger(LoggerInterface|LoggerType $a = new DatabaseLogger): static { if($a instanceof LoggerInterface) { $this->log = $a; return $this; } $this->log = $a->getLogger(); return $this; } } (new A)->withLogger((default)->log('B')); This would be valid under this RFC, right? 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. 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. > > On Aug 25, 2024, at 12:23 PM, John Coggeshall wrote: > > I won't vote for this RFC if the above code is valid, FWIW. Unlike include , default is a special-case with a very specific purpose -- one that is reaching into someone else's API in a way the developer of that library doesn't explicitly permit. > Ok, so if a developer of the API currently wants to indicate that other developers CAN explicitly reach in then how would they do that, currently? 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). > Your argument seems to be it should implicitly be disallowed, but I do not see any way in which you are proposing a developer could explicitly empower a developer to allow it. At least not without a ton of boilerplate which, for each default value, turns into a lot of extra code to create and then maintain. > > It seems one-sided to argue against allowing something that "has not been explicitly allowed" if there is no way to explicitly allow it. And I get you may say "well that's not my job" to which —if you did — I would ask "Do you really only want to be the one who brings the problem but not the solution?" Right now there isn't a problem. There is nothing lacking in the current language that prevents me from providing my own `LoggerInterface` or `LoggerType` to the `withLogger` above. This is a suggestion to expand the syntax of the language to simplify something, not enable it to exist where before it didn't. The RFC would introduce something that makes it marginally easier for a downstream consumer of the API, at the cost of making life painful for library authors and worse not providing any way to prevent it from happening. So yes, I am pointing out a problem but not providing a solution because I don't currently agree a solution is even needed. Not all ideas make the cut the first time, and that's okay. Those of us who have stood up against this idea in its current form have tried to offer our best thoughts as to how you might make it work by outlining that we think there need to be rules around the types of expressions that would be allowed... these opinions don't come out of nowhere or meant to be obstructionist -- still, they have largely been dismissed with strawman counters about how `include` statements let you do silly things, or responses like "The truth always lies in the code, which is why RFC authors are strongly encouraged to pursue patches where there is doubt, and similarly, I think counter-proposals on the mailing list should follow suit, otherwise we can find ourselves arguing over nothing." -- I don't agree with that, or feel obligated to attempt to write patches for an idea I don't think is necessary in the first place. John

On 26 August 2024 05:44:44 BST, Mike Schinkel <mike@newclarity.net> wrote:

On Aug 25, 2024, at 12:21 PM, Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> wrote:
The Reflection API is a bit like the Advanced Settings panel in a piece of software, it comes with a big "Proceed with Caution" warning. You only move something from that Advanced Settings panel to the main UI when it's going to be commonly used, and generally safe to use. I don't think allowing arbitrary operations on a value that's declared as the default of some other function passes that test.

You analogy is faulty. You are conflating a complex API — Reflection — with one use-case of that API which — per the RFC — has well-defined syntax and semantics and purely due to its simplicity is more likely to be used than the Reflection API and far less likely to be used incorrectly than the Reflection API.

You are misunderstanding the analogy. The analogy is that the Reflection API as a whole is like the Advanced Settings page, with its big "Proceed with Caution" sign. Accessing the default value of someone else's function is like a setting which you can only currently access on that advanced setting screen, but people are proposing we move to a big tempting tickbox on the main UI.

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.

On Aug 25, 2024, at 11:31 AM, Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> wrote:

Can you give a specific example from code we are likely to find in a production application — vs. a forum debate — where someone is likely to use `default+1` AND where it would be problematic for the author?

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".

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') { ... } }

Perhaps that would be more obvious if the declaration happened to look like this:

function foo(optional int $bar) {
    default for $bar is 42;
    // ...
}

As far as I can see, nobody has actually justified reading values out in this way, only said it's a side-effect of the current implementation.

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. I sometimes do that with private constants anyway, just to make the value more easily documented, or use it in a couple of different contexts.

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.

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]);

Rowan Tommins
[IMSoP]

On 25.08.2024 at 23:51, Bilge wrote:

If you believe I am incorrect about this, I encourage you to submit a
(working) Bison patch to demonstrate how a restricted expression grammar
subset can still recurse with the unrestricted superset, then we can
start having this discussion more seriously.

It seems to me that the restriction does not have be enforced by the
parser, but *could* be enforced during compilation of the AST. If that
*should* be done, is a different question.

Christoph

On 24.08.24 18:49, Bilge 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

Hello Paul,

I think this is an interesting addition to the language. Personally, I
would replace the full expression list at the end of the RFC with more
examples in real-world scenarios for most of these cases. As far as I
skimmed the discussion, there is some worry of "wrong use" (which I do
not necessarily share). Showing more examples could be useful to focus
on how having default being a full expression gives interesting use
cases, instead of talking about what (in isolation) nonsensical code
people might write.

For me there is another question. When using interfaces and classes,
default values can be introduced, like this:

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. 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. In the example
when getting CompressionInterface, I might test for the existence of a
default value of $level and leave it at the default if there is a
default (maybe I know that some implementations have a default value,
others don't). One could test the specific implementation with
instanceof checks, but the advantage of "default" could be that you do
not need to know the implementation and could only adapt to possibly
defined default values.

On 26/08/2024 09:58, Christoph M. Becker wrote:

On 25.08.2024 at 23:51, Bilge wrote:

If you believe I am incorrect about this, I encourage you to submit a
(working) Bison patch to demonstrate how a restricted expression grammar
subset can still recurse with the unrestricted superset, then we can
start having this discussion more seriously.

It seems to me that the restriction does not have be enforced by the
parser, but *could* be enforced during compilation of the AST. If that
*should* be done, is a different question.

Christoph

Thanks Christoph.

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.

Cheers,
Bilge

On 26/08/2024 10:03, Andreas Leathley wrote:

On 24.08.24 18:49, Bilge wrote:

For me there is another question. When using interfaces and classes,
default values can be introduced, like this:

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. 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. In the example
when getting CompressionInterface, I might test for the existence of a
default value of $level and leave it at the default if there is a
default (maybe I know that some implementations have a default value,
others don't). One could test the specific implementation with
instanceof checks, but the advantage of "default" could be that you do
not need to know the implementation and could only adapt to possibly
defined default values.

Hi Andreas,

Thanks for this question; I find this super interesting because it's something we haven't thought about yet. I must admit I completely overlooked that, whilst an interface /can/ require implementers to specify a default, in the case that they do not, it is still valid for implementations to selectively elect to provide one. Therefore I can append to your example the following case (I removed the `string` return type for now):

class ZipCompression implements CompressionInterface
{
public function compress(string $data, int $level)
{
var_dump($level);
}
}

new GzipCompression()->compress('', default ?? 6);
new ZipCompression()->compress('', default ?? 6);

In this case, we get the following output:

int(4)
Fatal error: Uncaught ValueError: Cannot pass default to required parameter 2 of ZipCompression::compress()

I would like to fix this if possible, because I think this should be valid, with emphasis on /if possible/, because it may be prohibitively complex. Will update later.

Cheers,
Bilge

That would be a way to fix it, to basically make isset(default) a possible check if there is no default, similar to an undefined variable check. It would also recognize a default value of null as not set in the same way, so one could not differentiate between null and not defined, but that is in line with the language in general.

···

On 26.08.24 11:26, Bilge wrote:

Thanks for this question; I find this super interesting because it’s something we haven’t thought about yet. I must admit I completely overlooked that, whilst an interface can require implementers to specify a default, in the case that they do not, it is still valid for implementations to selectively elect to provide one. Therefore I can append to your example the following case (I removed the string return type for now):

class ZipCompression implements CompressionInterface
{
public function compress(string $data, int $level)
{
var_dump($level);
}
}

new GzipCompression()->compress(‘’, default ?? 6);
new ZipCompression()->compress(‘’, default ?? 6);

In this case, we get the following output:

int(4)
Fatal error: Uncaught ValueError: Cannot pass default to required parameter 2 of ZipCompression::compress()

I would like to fix this if possible, because I think this should be valid, with emphasis on if possible, because it may be prohibitively complex. Will update later.

On 26/08/2024 11:11, Andreas Leathley wrote:

On 26.08.24 11:26, Bilge wrote:

I would like to fix this if possible, because I think this should be valid, with emphasis on /if possible/, because it may be prohibitively complex. Will update later.

That would be a way to fix it, to basically make isset(default) a possible check if there is no default, similar to an undefined variable check. It would also recognize a default value of null as not set in the same way, so one could not differentiate between null and not defined, but that is in line with the language in general.

It would not be possible to write `isset(default)` because `isset()` does not operate on expressions. Similarly, it would not be possible to write `default === null ? ... : ...` because you would receive the same error as above. If we special-case null coalesce then it will literally only be possible to check with null coalesce.

As for feasibility, I definitely believe it is feasible. Though some may argue whether it's worth the trouble for this edge case, I would still like to implement it.

Cheers,
Bilge

Hey folks.

Am 26.08.24 um 11:26 schrieb Bilge:

On 26/08/2024 10:03, Andreas Leathley wrote:

On 24.08.24 18:49, Bilge wrote:

For me there is another question. When using interfaces and classes,
default values can be introduced, like this:

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. 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. In the example
when getting CompressionInterface, I might test for the existence of a
default value of $level and leave it at the default if there is a
default (maybe I know that some implementations have a default value,
others don't). One could test the specific implementation with
instanceof checks, but the advantage of "default" could be that you do
not need to know the implementation and could only adapt to possibly
defined default values.

Hi Andreas,

Thanks for this question; I find this super interesting because it's something we haven't thought about yet. I must admit I completely overlooked that, whilst an interface /can/ require implementers to specify a default, in the case that they do not, it is still valid for implementations to selectively elect to provide one. Therefore I can append to your example the following case (I removed the `string` return type for now):

class ZipCompression implements CompressionInterface
{
public function compress(string $data, int $level)
{
var_dump($level);
}
}

new GzipCompression()->compress('', default ?? 6);
new ZipCompression()->compress('', default ?? 6);

In this case, we get the following output:

int(4)
Fatal error: Uncaught ValueError: Cannot pass default to required parameter 2 of ZipCompression::compress()

I would like to fix this if possible, because I think this should be valid, with emphasis on /if possible/, because it may be prohibitively complex. Will update later.

Cheers,
Bilge

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.

*Or* we are coding against the actual implementations. Then it is totally valid IMO that providing `default` when no default is set in the concrete implementation of the function signature raises a fatal error.

So in the example above it's either

new GzipCompression()->compress('', default ?? 6);
new ZipCompression()->compress('', default ?? 6);

and it is absolutely valid that the second one triggers a fatal error as no default is set in the concrete implementation.

Or it's something like

/** @var CompressionInterface $compression */
foreach ([new GzipCompression, new ZipCompression] as $compression) {
     $compression->compress('', default ?? 6)
}

in which case it should definitely fail as the interface doesn't provide a default.

Cheers

Andreas

--
                                                               ,
                                                              (o o)
+---------------------------------------------------------ooO-(_)-Ooo-+
| Andreas Heigl |
| mailto:andreas@heigl.org N 50°22'59.5" E 08°23'58" |
| https://andreas.heigl.org |
+---------------------------------------------------------------------+
| Appointments - Nextcloud |
+---------------------------------------------------------------------+
| GPG-Key: https://hei.gl/keyandreasheiglorg |
+---------------------------------------------------------------------+

It’s pretty useful for testing.

···

On 26/08/2024 08:28, Rowan Tommins [IMSoP] wrote:

As far as I can see, nobody has actually justified reading values out in this way, only said it's a side-effect of the current implementation.
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]);

Thanks for this interesting question. Here is the output from your (slightly modified) script:

class Something {}
function foo(array|Something $x=new Something) {return $x;}
var_dump($x = foo([default, default, default]));
var_dump($x[0] === $x[1]);

array(3) {
[0]=>
object(Something)#1 (0) {
}
[1]=>
object(Something)#2 (0) {
}
[2]=>
object(Something)#3 (0) {
}
}
bool(false)

As you can observe from the object hashes, each object is unique. That is, we fetch the default value each time the keyword appears, which in the case of objects, creates a new instance each time. I will update the RFC with this information.

Cheers,
Bilge