Hi gang,
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.
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 gang,
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.
This is a feature I've wanted for a very long time! The RFC is very
straight forward, and the appendix does a great job of enumerating the
possible expressions.
Nice work all around!
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
This is a feature I've wanted for a very long time! The RFC is very
straight forward, and the appendix does a great job of enumerating the
possible expressions.Nice work all around!
Thanks, Matt! Glad you like this one (and the last one!)
Hopefully we can land it this time 🙂
Cheers,
Bilge
Hi gang,
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.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
This is pretty awesome! I see this as some syntax sugar, to be honest:
as soon as two or more nullable arguments are involved
I'm not sure what you mean here. I use this method all the time :) much to the chagrin of some of my coworkers.
function stuff($foo = 'bar', $baz = 'world');
stuff(...[ ...($foo ? ['foo' => $foo] : []), ...($baz ? ['baz' => $baz] : [])]);
Having this would be a lot less verbose.
— Rob
as soon as two or more nullable arguments are involved
I'm not sure what you mean here. I use this method all the time :)
much to the chagrin of some of my coworkers.function stuff($foo = 'bar', $baz = 'world');
stuff(...[ ...($foo ? ['foo' => $foo] : []), ...($baz ? ['baz' =>
$baz] : [])]);
You're right; splat with keys does work. This is something my RFC
currently glosses over and should probably be rectified!
Cheers,
Bilge
I'm not sure what you mean here. I use this method all the time :) much to the chagrin of some of my coworkers.
function stuff($foo = 'bar', $baz = 'world');
stuff(...[ ...($foo ? ['foo' => $foo] : []), ...($baz ? ['baz' => $baz] : [])]);
And you are one who complains about gotos! 😲
-Mike
I'm not sure what you mean here. I use this method all the time :) much to the chagrin of some of my coworkers.
function stuff($foo = 'bar', $baz = 'world');
stuff(...[ ...($foo ? ['foo' => $foo] : []), ...($baz ? ['baz' => $baz] : [])]);
And you are one who complains about gotos! 😲
-Mike
Haha, there is a difference between production/professional code and internal tools. Internal tools are a place to experiment and have a little fun, IMHO.
— Rob
Hi gang,
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.
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
Great RFC, Bilge! I was already on-board after the introduction, but if I had any doubts, the examples in the appendix sold me.
Cheers,
Ben
Great RFC, Bilge! I was already on-board after the introduction, but if I had any doubts, the examples in the appendix sold me.
Cheers,
Ben
Thanks, Ben. That means a lot to me :)
Cheers,
Bilge
(resending as I accidentally originally send a private reply instead of
sending the below to the list)
Hi gang,
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.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 Bilge,
I like the idea, but see some potential for issues with ambiguity, which
I don't see mentioned in the RFC as "solved".
Example 1:
function foo($paramA, $default = false) {}
foo( default: default ); // <= Will this be handled correctly ?
Example 2:
callme(
match($a) {
10 => $a * 10,
20 => $a * 20,
default => $a * default, // <= Based on a test in the PR this
should work. Could you confirm ?
}
);
Example 3:
switch($a) {
case 'foo':
return callMe($a, default); // I presume this shouldn't be a
problem, but might still be good to have a test for this ?
default:
return callMe(10, default); // I presume this shouldn't be a
problem, but might still be good to have a test for this ?
}
On that note, might it be an idea to introduce a separate token for the
default
keyword when used as a default expression in a function call
to reduce ambiguity ?
Smile,
Juliette
(resending as I accidentally originally send a private reply instead
of sending the below to the list)Hi gang,
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.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,
BilgeHi Bilge,
Hi :)
I like the idea, but see some potential for issues with ambiguity,
which I don't see mentioned in the RFC as "solved".Example 1:
function foo($paramA, $default = false) {} foo( default: default ); // <= Will this be handled correctly ?
No, but not because of my RFC, but because $paramA is a required
parameter that was not specified. Assuming that was just a typo, the
following works as expected:
function foo($paramA = 1, $default = false) {
var_dump($default);
}
foo(default: default); // bool(false)
Example 2:
callme( match($a) { 10 => $a * 10, 20 => $a * 20, default => $a * default, // <= Based on a test in the PR this should work. Could you confirm ? } );
Yes.
Example 3:
switch($a) { case 'foo': return callMe($a, default); // I presume this shouldn't be a problem, but might still be good to have a test for this ? default: return callMe(10, default); // I presume this shouldn't be a problem, but might still be good to have a test for this ? }
Yes.
On that note, might it be an idea to introduce a separate token for
thedefault
keyword when used as a default expression in a function
call to reduce ambiguity ?
Considering the Bison grammar compiles, I believe there can be no
ambiguity. I specifically pickeddefault
because I think it is the
most intuitive keyword to use for this, and it's conveniently already a
reserved word.
Cheers,
Bilge
(resending as I accidentally originally send a private reply instead
of sending the below to the list)Hi gang,
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.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,
BilgeHi Bilge,
Hi :)
I like the idea, but see some potential for issues with ambiguity,
which I don't see mentioned in the RFC as "solved".Example 1:
function foo($paramA, $default = false) {} foo( default: default ); // <= Will this be handled correctly ?
No, but not because of my RFC, but because $paramA is a required
parameter that was not specified. Assuming that was just a typo, the
following works as expected:function foo($paramA = 1, $default = false) {
var_dump($default);
}
foo(default: default); // bool(false)Example 2:
callme( match($a) { 10 => $a * 10, 20 => $a * 20, default => $a * default, // <= Based on a test in the PR this should work. Could you confirm ? } );
Yes.
Example 3:
switch($a) { case 'foo': return callMe($a, default); // I presume this shouldn't be a problem, but might still be good to have a test for this ? default: return callMe(10, default); // I presume this shouldn't be a problem, but might still be good to have a test for this ? }
Yes.
On that note, might it be an idea to introduce a separate token for
thedefault
keyword when used as a default expression in a function
call to reduce ambiguity ?
Considering the Bison grammar compiles, I believe there can be no
ambiguity. I specifically pickeddefault
because I think it is the
most intuitive keyword to use for this, and it's conveniently already a
reserved word.
Other tools parse the tokens directly (for example, I have a tool to take php classes and convert them to graphql specifications. It parses the tokens emitted the tokenization extension), and having "default" tokens in unexpected places presents an ambiguity and BC break for those tools. By having a token (DEFAULT_PARAM_VALUE) or something to disambiguate might be better. I had assumed it was a separate token when I first read it, so this is a good point.
— Rob
Hi gang,
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.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
I am still not fully sold on this, but I like it a lot better than the previous attempt at a default keyword. It's good that you mention named arguments, as those do replace like 95% of the use cases for "put default here" in potential function calls, and the ones it doesn't, you call out explicitly as the justification for this RFC.
The approach here seems reasonable overall. The mental model I have from the RFC is "yoink the default value out of the function, drop it into this expression embedded in the function call, and let the chips fall where they may." Is that about accurate?
My main holdup is the need. I... can't recall ever having a situation where this is something I needed. Some of the examples show valid use cases (eg, the "default plus this binary flag" example), but again, I've never actually run into that myself in practice.
My other concern is the list of supported expression types. I understand how the implementation would naturally make all of those syntactically valid, but it seems many of them, if not most, are semantically nonsensical. Eg, default > 1
would take a presumably numeric default value and output a boolean, which should really never be type compatible with the function being called. (A param type of int|bool is a code smell at best, and a fatal waiting to happen at worst.) In practice, I think a majority of those expressions would be logically nonsensical, so I wonder if it would be better to only allow a few reasonable ones and block the others, to keep people from thinking nonsensical code would do something useful.
--Larry Garfield
Hi gang,
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.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,
BilgeI am still not fully sold on this, but I like it a lot better than the previous attempt at a default keyword. It's good that you mention named arguments, as those do replace like 95% of the use cases for "put default here" in potential function calls, and the ones it doesn't, you call out explicitly as the justification for this RFC.
The approach here seems reasonable overall. The mental model I have from the RFC is "yoink the default value out of the function, drop it into this expression embedded in the function call, and let the chips fall where they may." Is that about accurate?
My main holdup is the need. I... can't recall ever having a situation where this is something I needed. Some of the examples show valid use cases (eg, the "default plus this binary flag" example), but again, I've never actually run into that myself in practice.
Potentially the most useful place would be in attributes. Take crell\serde (:p) for instance:
#[SequenceField(implodeOn: default . ' ', joinOn: ' ' . default . ' ')]
Where you may just want it to be a little more readable, but aren't interested in the default implosion. In attributes, it has to be a static expression and I think this passes that test? At least that is one place I would find most useful.
Then there are things like the example I gave before, where you need to call some library code as library code and pass through the intentions. It also gets us one step closer to something like these shenanigans:
function configureSerializer(Serde $serializer = new SerdeCommon(formatters: default as $formatters));
Where we can call configureSerializer(formatters: new JsonStreamFormatter()).
Some pretty interesting stuff.
My other concern is the list of supported expression types. I understand how the implementation would naturally make all of those syntactically valid, but it seems many of them, if not most, are semantically nonsensical. Eg,
default > 1
would take a presumably numeric default value and output a boolean, which should really never be type compatible with the function being called. (A param type of int|bool is a code smell at best, and a fatal waiting to happen at worst.) In practice, I think a majority of those expressions would be logically nonsensical, so I wonder if it would be better to only allow a few reasonable ones and block the others, to keep people from thinking nonsensical code would do something useful.
I'm reasonably certain you can write nonsensical PHP without this feature. I don't think we should be the nanny of developers.
— Rob
The approach here seems reasonable overall. The mental model I have from the RFC is "yoink the default value out of the function, drop it into this expression embedded in the function call, and let the chips fall where they may." Is that about accurate?
Yes, as it happens. That is the approach we took, because the
alternative would have been changing how values are sent to functions,
which would have required a lot more changes to the engine with no clear
benefit. Internally it literally calls the reflection API, but a
low-level call, that elides the class instantiation and unnecessary
hoops of the public interface that would just slow it down.
My main holdup is the need. I... can't recall ever having a situation where this is something I needed. Some of the examples show valid use cases (eg, the "default plus this binary flag" example), but again, I've never actually run into that myself in practice.
That's fine. Not everyone will have such a need, and of those that do,
I'm willing to bet it will be rare or uncommon at best. But for those
times it is needed, the frequency by which it is needed in no way
diminishes its usefulness.I rarely usegoto
but that doesn't mean we
shouldn't have the feature.
My other concern is the list of supported expression types. I understand how the implementation would naturally make all of those syntactically valid, but it seems many of them, if not most, are semantically nonsensical. Eg,default > 1
would take a presumably numeric default value and output a boolean, which should really never be type compatible with the function being called. (A param type of int|bool is a code smell at best, and a fatal waiting to happen at worst.) In practice, I think a majority of those expressions would be logically nonsensical, so I wonder if it would be better to only allow a few reasonable ones and block the others, to keep people from thinking nonsensical code would do something useful.
Since you're not the only one raising this, I will address it, but just
to say there is no good reason, in my mind, to ever prohibit the
expressiveness. To quote Rob
I'm reasonably certain you can write nonsensical PHP without this
feature. I don't think we should be the nanny of developers.
I fully agree with that sentiment. It seems to be biting me that I went
to the trouble of listing out every permutation of what /expression/
means where perhaps this criticism would not have been levied at all had
I chosen not to do so. Why does that matter? Because PHP already allows
you to do many more ridiculous things, they're just not routinely
presented to you so they're not part of your mind map. The end-user
documentation will also not mention the nonsense cases, so the average
developer will not think of them. You can write, include(1 + 1);
,
because include()
accepts an expression. You will get: "Failed opening
'2' for inclusion". Should we restrict that? No, because that's just how
expressions work in any context where they're allowed. Special-casing
the T_DEFAULT
grammar would not only bloat the grammar rules but also
increase the chance that new expression grammars introduced in future,
which could conveniently interoperate with default
, would be
unintentionally excluded by omission.
Cheers,
Bilge
You can write,
include(1 + 1);
, becauseinclude()
accepts an
expression. You will get: "Failed opening '2' for inclusion". Should
we restrict that? No, because that's just how expressions work in any
context where they're allowed.
I think a better comparison might be the "new in initializers" and
"fetch property in const expressions" RFCs, which both forbid uses which
would naturally be allowed by the grammar. The rationale in those cases
was laid out in
https://wiki.php.net/rfc/new_in_initializers#unsupported_positions and
https://wiki.php.net/rfc/fetch_property_in_const_expressions#supporting_all_objects
To pull out a point that might be overlooked at the bottom of my longer
response earlier:
As the RFC points out, library authors already worry about the
maintenance burden of named argument support, will they now also need to
question whether someone is relying on "default + 1" having some
specific effect?
By saying "default can be used in any expression, as complex as the
caller can imagine", we're implicitly saying "if you add a default to
your function signature, that is no information a user can pull out as
part of your API".
Regards,
--
Rowan Tommins
[IMSoP]
Hi Rowan
On Sun, Aug 25, 2024 at 6:06 PM Rowan Tommins [IMSoP]
imsop.php@rwec.co.uk wrote:
You can write,
include(1 + 1);
, becauseinclude()
accepts an
expression. You will get: "Failed opening '2' for inclusion". Should
we restrict that? No, because that's just how expressions work in any
context where they're allowed.I think a better comparison might be the "new in initializers" and
"fetch property in const expressions" RFCs, which both forbid uses which
would naturally be allowed by the grammar. The rationale in those cases
was laid out in
https://wiki.php.net/rfc/new_in_initializers#unsupported_positions and
https://wiki.php.net/rfc/fetch_property_in_const_expressions#supporting_all_objects
I don't agree with that. Constant expressions in PHP already only
support a subset of operations that expressions do. However, default
is proposed to be a true expression, i.e. one that compiles to
opcodes. Looking at the expr
nonterminal [1] I can't see any
productions that are restricted in the context they can be used in,
even though plenty of them are nonsensical (e.g. exit(1) + 2).
Furthermore, new in initializers was disallowed in some contexts not
because it would be nonsensical, but because it posed technical
difficulties.
I also believe some of the rules you've laid out would be hard to enforce.
- The expression should be reasonably guaranteed to produce the same type as the actual default.
Even the simple cases of ??, ?: can easily break this rule.
Furthermore, context restriction is easily circumvented. E.g.
foo((int) default); // This is not allowed
foo((int) match (true) { default => default }); // Let me just do that
I'm not sure context restriction is worthwhile, if 1. we can't do it
properly anyway and 2. there are no technical reasons to do so.
Ilija
I don't agree with that. Constant expressions in PHP already only
support a subset of operations that expressions do. However, default
is proposed to be a true expression, i.e. one that compiles to
opcodes.
This is circular: obviously, changing the proposal requires making
changes to what is proposed.
I'm arguing that allowing default as a token that's usable in arbitrary
expressions is unnecessary and problematic, and that we should instead
define the specific use cases, and build the feature around those.
I also believe some of the rules you've laid out would be hard to enforce.
The rules were intended to guide the design of the feature, not be
things that someone needed to enforce in code somewhere. If you start
with the aim of implementing:
- Use in place of an argument
- Use with bitwise | and &
- Use on the RHS of ?: and ??
Then maybe you end up with a completely different implementation from
what's currently been written.
For instance, rather than adding "default" to the "expr" rule in the
grammar, and then restricting it at compile-time, maybe we add a new
grammar rule "expr_with_default", usable only in expressions and with a
very limited set of productions. Maybe that means we can't support
match() expressions, because it would bloat the grammar too much, but
"match($foo) { 'blah'=> 'bleugh', default => default }" is pretty ugly
anyway.
Or maybe, the expressions are allowed, but they're compiled down with
"default" as a special pseudo-type that has limited legal operations, so
that the result of "(int)default" is undefined, no matter how you try to
obfuscate it.
Just because it's easy to implement a feature a particular way, doesn't
mean that's necessarily the right way.
--
Rowan Tommins
[IMSoP]
For instance, rather than adding "default" to the "expr" rule in the
grammar, and then restricting it at compile-time, maybe we add a new
grammar rule "expr_with_default", usable only in expressions and with
a very limited set of productions.
Like the original commit
https://github.com/php/php-src/pull/15437/commits/fd7ac5f83b8282227235095843cb73e9d66b0717#diff-3e6742a9069b5717cf961c9d6b2aefbd1c730869d8a58123b1b5f3bc3e9082fcR1329?
Yeah, we did that.
Just because it's easy to implement a feature a particular way,
doesn't mean that's necessarily the right way.
With respect, you do not know what you're talking about here. The
original approach was to start manually whitelisting each expression
grammar I thought made sense. THAT was the easiest way because both
myself an Ilija failed in our first attempts to expand the grammar to
support default as a general expression, and not for lack of trying. It
took a Bison grammar expert to drop a patch that certainly wowed me,
because hitherto I wasn't even certain it was possible, mainly because
of the conflicts with match
(but also switch
to some extent). Aside,
with respect to match, there is still an unresolved case and the RFC
needs updating with the semantics we want to enforce there. So we
pursued default as an expression not because it was easy, but despite
the fact that it was hard, because it was precisely what we wanted to do.
I apologise for coming on strong, but I put a lot of effort into this,
so I take exception to the implication that anyone involved took the
easy way out to arrive at this (our best) solution.
Kind regards,
Bilge
I apologise for coming on strong, but I put a lot of effort into this,
so I take exception to the implication that anyone involved took the
easy way out to arrive at this (our best) solution.
I apologise for the inadvertent offence.
It was based solely on this comment from Ilija:
I also believe some of the rules you've laid out would be hard to
enforce.
I took that to mean that supporting generic expressions was
straight-forward, but supporting a limited set would be complex in some
way. Apparently I was wrong in that interpretation; in which case, I've
no idea what that sentence was referring to.
--
Rowan Tommins
[IMSoP]
I apologise for coming on strong, but I put a lot of effort into
this, so I take exception to the implication that anyone involved
took the easy way out to arrive at this (our best) solution.I apologise for the inadvertent offence.
That's OK, I can tell you're passionate about PHP and you're interested
in having a constructive discussion about this RFC, so we have that in
common 🙂
Cheers,
Bilge
You can write,
include(1 + 1);
, becauseinclude()
accepts an
expression. You will get: "Failed opening '2' for inclusion". Should
we restrict that? No, because that's just how expressions work in any
context where they're allowed.I think a better comparison might be the "new in initializers" and
"fetch property in const expressions" RFCs, which both forbid uses
which would naturally be allowed by the grammar. The rationale in
those cases was laid out in
https://wiki.php.net/rfc/new_in_initializers#unsupported_positions and
https://wiki.php.net/rfc/fetch_property_in_const_expressions#supporting_all_objects
They do not seem like better comparisons because, in both cases, support
for those respective features was limited due to technical obstructions.
My implementation already permits default
as a general expression
(thanks to Bob's Bison patch), Q.E.D. there is no technical constraint
precluding support for default as a general expression grammar. What you
are proposing is an artificial limitation on the language, which is an
entirely different proposition (and not a healthy one, in my view).
Allow me to address the point made in your previous email which ended up
hinting that default + 1
should also be prohibited because it hasn't
been explicitly justified.
Notwithstanding I don't have the energy to justify every single
permutation of expressions, I'll humour the arithmetic operators
criticism with an example just to demonstrate that one can justify just
about anything with sufficient enthusiasm and creativity.
Suppose we have a Suspension class that suspends the current process for
a specified delay in milliseconds, but our subclass wants to present an
interface that deals with whole seconds (including fractional seconds
using floats).
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.
Cheers,
Bilge
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.
I appreciate what you're saying here.
I've been struggling a little bit to really nail my language here on what I think should and shouldn't be allowed. Essentially I'm trying to say (and I think others are too) is this:
The engine should not allow the use of default in an expression that doesn't ultimately evaluate to default *.
In the above example the left - hand of the ?: operator doesn't use default , so it's evaluation is whatever it's evaluation is. The right-hand of the ?: operator DOES use default , and thus it must evaluate ultimately to default or that would be an error. Another example:
parent::foo((default >= 10) ? default : 10)
Would be permitted because the left-hand uses default , but the evaluation if the conditional where default was used for the true case true is default . Likewise the right-hand is just 10 and irrelevant
This would not be permitted
parent::foo((default >= 10) ? (default + 1) : 10)
Because now the ultimate evaluation of default is default + 1 -- not default
*The exception to the rule I've described above would be IFF the expression default is in only uses specific allowed operators like a subset of the bitwise operators.
I very much appreciate that what is being described here is a significant effort to achieve, I'm not even sure it's reasonably possible.. 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. The use of this proposed default keyword must have guardrails IMO. I think my definition above is a pretty reasonable attempt at capturing where I think the line is here and hopefully that helps guide this discussion.
John
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]
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
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]
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
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]
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.
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.
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:
- Some expressions make no sense so we should disallow them, and
- 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.
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:
- "How trivial vs. damaging is a thing" and
- "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.
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));
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?"
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.
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
?
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:
-
Modifying a default bitmapped argument to add or remove one or more bits.
-
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)
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 havefoo(int $index=1)
, a developer calls withfoo(default*3)
, and then the author of foo changes the signature tofoo(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.
- "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 disallowdefault!=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.
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
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 withfoo(default*3)
, and then the author of foo changes the signature tofoo(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:
- The purpose of the function,
- Why someone would multiply the default times three, nor
- 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?" ;-)
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:
- There is a parameter with a default value,
- That parameter is type-hinted,
- The hinted type is declared as a union type,
- An earlier version of the library initialized the default with a value having one of the union types,
- End-user developers used the library and then use
default
as an expression of that type, and finally - 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:
- Should defaults just be assumed to be part of the public API, and
- 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.
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. ;-)
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.
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!
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
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]
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?
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. ;-)
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. :-)
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:
<snip>
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?
From the example you gave it appears that we can have a concrete problem when:
- There is a parameter with a default value,
- That parameter is type-hinted,
- The hinted type is declared as a union type,
- An earlier version of the library initialized the default with a value having one of the union types,
- End-user developers used the library and then use
default
as an expression of that type, and finally- 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.
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!
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.
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.) :-)
-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.
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. :-)
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]
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]
From the example you gave it appears that we can have a concrete problem when:
- There is a parameter with a default value,
- That parameter is type-hinted,
- The hinted type is declared as a union type,
- An earlier version of the library initialized the default with a value having one of the union types,
- End-user developers used the library and then use
default
as an expression of that type, and finally- 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).
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.
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]
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.
It's pretty useful for testing.
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
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
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.
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
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
You're absolutely right, I would be interested to see any viable patch
that effectively implements a set of restrictions on howdefault
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, Aug 26, 2024 at 12:49 PM Rowan Tommins [IMSoP] imsop.php@rwec.co.uk
wrote:
You're absolutely right, I would be interested to see any viable patch
that effectively implements a set of restrictions on howdefault
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
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
On Mon, Aug 26, 2024 at 12:49 PM Rowan Tommins [IMSoP]
imsop.php@rwec.co.uk wrote:You're absolutely right, I would be interested to see any viable patch
that effectively implements a set of restrictions on howdefault
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
On Mon, Aug 26, 2024, 12:02 PM Larry Garfield larry@garfieldtech.com
wrote:
On Mon, Aug 26, 2024 at 12:49 PM Rowan Tommins [IMSoP]
imsop.php@rwec.co.uk wrote:You're absolutely right, I would be interested to see any viable
patch
that effectively implements a set of restrictions on howdefault
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
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.
This seems like a valid and balanced position from Larry.
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
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)
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.
I have to be honest all of these both sound like really bad practices. Maybe I'm just not being imaginative enough... but if you don't control the upstream library why would you ever trust it like that? Writing a timeout default * 0.5 when default is 1000 is a really bad way to set your timeout to 500 -- because next time you done a composer update suddenly the upstream package set the timeout to 5000 and now instead of 500 your timeout has silently become 2500.
- decorating a default instance (e.g. to lazily create a proxy without knowing the default implementation used for an argument hinted against an interface)
This is exactly the usage I'm highlighted as problematic in my code example. You're introducing a new worry for the upstream API developer that doesn't need to exist, and violating a separation principle that has existed in PHP since default parameters were created 25+ years ago.
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.
If this feature is released with an overly broad scope in terms of expressions, etc. it's not like we can take it back at that point because now people are using it in unknown ways. It is not one I'm comfortable with a "let's move forward and see what happens" approach.
I don't think this RFC is anywhere near a state of being ready for a vote and I'd like to see it go back and be updated with all this feedback before we continue going back and forth on the mailing list about it. This isn't a small feature, despite the fact on the surface it seems relatively minor as compared to other things. It is introducing a whole new concept into PHP of, at least in some circumstances, suddenly making the default parameter in a function signature a formal part of it and of utmost importance that API developers have to think about changing. It's a big deal.
Offhand, I am unaware of what other languages have done this or allow anything like it (are there any?) -- and there have been demonstrated real concerns backed with code examples of how this becomes problematic. Not to mention the fact it would retroactively make all default parameters in all PHP code suddenly accessible by downstream consumers in a way the author simply never intended for. As others have mentioned, this is just as big of a deal as if there was an RFC that suddenly gave developers a syntax to access object's private methods by adding ! to the property name or something (e.g. an RFC that proposed accessing private members of an object just by doing $foo->myPrivate! ).
I am being left with the impression based on these emails that those in favor of this are hand-waving past some very legitimate concerns trying to push it through, rather than taking the appropriate action of accepting the feedback in the collaborative fashion it was intended and going back to the RFC to address those concerns fully. Perhaps that impression is mistaken, and if so I apologize... But to be clear I don't think there is anywhere near enough consensus on this RFC to make this a slam dunk, nor do I think it's a few tweaks away from being adoptable.
On Aug 26 2024, at 2:11 pm, Matthew Weier O'Phinney <
mweierophinney@gmail.com> wrote: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.I have to be honest all of these both sound like really bad practices.
Maybe I'm just not being imaginative enough... but if you don't control the
upstream library why would you ever trust it like that? Writing a timeout default
- 0.5 when default is 1000 is a really bad way to set your timeout to
500 -- because next time you done a composer update suddenly the
upstream package set the timeout to 5000 and now instead of 500 your
timeout has silently become 2500.
You'll likely identify the increased delay in such cases. Generally
speaking these sorts of default values don't change a ton, but it's not
unlikely that you may say "I'd like half that delay" or "twice that delay".
But a better example would be introducing a backoff, which might look like
this:
for ($i = 1; $i += 1 ; $i < 4) {
$result = call_some_client($url, default * $i);
if ($result->isSuccess()) {
break;
}
}
In other words, you know that you want it to use the default, and then
allow an increasing timeout duration between calls if it fails. For this, I
don't necessarily want or need to know what the default is, only that I
want to do multiples of it in specific cases.
Is it a universally good idea? No. Does it have use cases? Yes.
- decorating a default instance (e.g. to lazily create a proxy without
knowing the default implementation used for an argument hinted against an
interface)This is exactly the usage I'm highlighted as problematic in my code
example. You're introducing a new worry for the upstream API developer that
doesn't need to exist, and violating a separation principle that has
existed in PHP since default parameters were created 25+ years ago.
How exactly is this worrisome? Consider this:
class A {
public function __construct(private LogInterface $logger = new
DefaultLogger()) { }
}
class ProxiedLogger implements LogInterface { ... }
$a = new A(new ProxyLogger(default));
If class A is programming to the LogInterface
contract, the fact that it
gets a proxied version of the default should not matter in the least. Being
able to proxy like this means that a consumer of class A does not need to
know or care what the default implementation is; they can assume it follows
the same contract, and proxy to it regardless. The upstream developer
doesn't need to care, because they are programming to the interface, not
the implementation. This doesn't violate the separation of concerns
principle, nor covariance.
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.If this feature is released with an overly broad scope in terms of
expressions, etc. it's not like we can take it back at that point because
now people are using it in unknown ways. It is not one I'm comfortable with
a "let's move forward and see what happens" approach.
I didn't say that at all. I said we should identify the ones we absolutely
know will be problematic, and restrict those from the outset. From there,
we should identify the ones that might be problematic, and determine on a
case by case basis if the risks outweigh the use cases before Bilge brings
it to a vote.
But if we lock it down too tightly from the outset, expanding it, while
being possible, will more than likely mean an RFC for every expansion,
because it's unlikely somebody will do anything comprehensive towards
opening it up in the future. I'd rather not leave some of these use cases
as a TBD for a later RFC, because that's very likely going to mean "never".
I DO think there are likely whole categories of expressions we can likely
say "no" to - anything where the default represents a union type (and
mixed is a union type) should likely only allow default by itself or as a
bare value on the RHS of an expression.
The argument against the feature that it expands the public API is puzzling
to me, particularly when the only other solutions are (a) Reflection, or
(b) named arguments. Named arguments are part of the public API, as the
names themselves can change. Default values can change, but, importantly, a
change in a default value does not change the actual signature of a
function. Giving the ability to use default
gives consumers of a function
a far more stable API surface, particularly when they do not want to change
a given default value, but do want to provide a more specific value for a
later argument. Right now, if not using named arguments (e.g., because
you're worried the argument name could change), your only other option is
to use the Reflection API, which is more expensive, introduces a whole set
of other possible runtime issues, and is far more convoluted to achieve.
Without this RFC, those are your only options.
Had this been only to allow the default
keyword, I don't think we'd be
having this discussion at all; I think it would be pretty self-evident that
there's a need, and a lot of folks would be happy to sign on.
But the author took it a step further, and asked, "What if ...?", and as
such, the RFC provides additional benefits beyond giving you a keyword for
using the default value, as it expands to allowing expressions. This gives
a tremendous amount of flexibility and power, and solves some additional
issues some of us have noticed.
So I'd argue that what we need to weigh now is which of these expressions
are truly benefits, which ones are side effects we can live with, and which
will raise new problems we do not want to deal with.
Again, there are a few lists going around on this thread, and I hope that
Bilge is taking notes to add to the RFC, and working with the folks who
helped him with implementation to determine what is and is not possible in
terms of the grammar so we can potentially exclude the more problematic
ones.
But let's not just say "no expressions" - I think there's been some
demonstrated value from a lot of these.
--
Matthew Weier O'Phinney
mweierophinney@gmail.com
https://mwop.net/
he/him
Again, there are a few lists going around on this thread, and I hope
that Bilge is taking notes to add to the RFC, and working with the
folks who helped him with implementation to determine what is and is
not possible in terms of the grammar so we can potentially exclude the
more problematic ones.
I am just about to amend the RFC with the major discussion points from
detractors, however I am still missing a list of even one item that must
absolutely be prohibited, along with an explanation as to why.
FWIW, I have had the grand fortune to work with, or receive direct
feedback from, some of the most talented PHP heads in the scene (read:
engine maintainers) and they (so far) have unanimously confessed they do
not see any issue with any permutation of default as an expression.
Indeed, the entire point of the expression grammar is they can be
composed freely with one another in any way you like; to do otherwise
would be radical departure from the intended behaviour.
In case it matters, my initial inclination was also to do what some
others have suggested, and modify the SEND opcodes so that the default
is not actually looked up using reflection at all, but rather we just
send nothing to the function and it can use its default as it would
normally, but since I had the good sense to ask an engine maintainer how
they would approach this problem, they cautioned me that this was the
approach Stas took 10 years ago
https://github.com/php/php-src/compare/master...smalyshev:php-src:skip_params7,
that that approach was horrendous (paraphrasing) and they would never
support something like that (mainly because there's like 11 of them and
modifying them all would be a complete mess). So whilst you might find
me quite unmoved by some of the arguments put forth on the mailing list
and generally difficult to convince, it is not because I am stubborn in
my naivete, it is because when wisdom was imparted, I listened. I am but
a small man standing on the shoulders of giants.
Kind regards,
Bilge
In case it matters, my initial inclination was also to do what some others have suggested, and modify the SEND opcodes so that the default is not actually looked up using reflection at all, but rather we just send nothing to the function and it can use its default as it would normally, but since I had the good sense to ask an engine maintainer how they would approach this problem, they cautioned me that this was the approach Stas took 10 years ago (https://github.com/php/php-src/compare/master...smalyshev:php-src:skip_params7), that that approach was horrendous (paraphrasing) and they would never support something like that (mainly because there's like 11 of them and modifying them all would be a complete mess). So whilst you might find me quite unmoved by some of the arguments put forth on the mailing list and generally difficult to convince, it is not because I am stubborn in my naivete, it is because when wisdom was imparted, I listened. I am but a small man standing on the shou
lders of giants.
The RFC for Stas' implementation of default was very different not only in approach, but it was put forth at a different time to solve a different problem. I don't think it makes sense to conflate the two issues. Very importantly Stas was not proposing an expression syntax that increased visibility and access into function declaration defaults as you are proposing.
https://wiki.php.net/rfc/skipparams
I am just about to amend the RFC with the major discussion points from detractors, however I am still missing a list of even one item that must absolutely be prohibited, along with an explanation as to why.
I've seen multiple explanations by many and I know I personally put a code example forth. I am not sure what prompts you to say that.
I'd be very interested in hearing from you, as the RFC author, specifically about that code example -- and why you believe the issue I described is worth the cost.
Indeed, the entire point of the expression grammar is they can be composed freely with one another in any way you like; to do otherwise would be radical departure from the intended behaviour.
I think what has been lost in this conversation is that the "detractors" believe the intended behavior is not a good direction to take the language. It seems to me that we simply don't think default parameters should be directly accessible to the caller in a major percentage of the circumstances. Any discussion about expression restrictions or the like has only happened because those same detractors are offering potential compromise ideas. The fundamental problem with the RFC to me is that it's creating an access into function declarations that IMO is neither needed or wise.
Coogle
In case it matters, my initial inclination was also to do what some others
have suggested, and modify the SEND opcodes so that the default is not
actually looked up using reflection at all, but rather we just send nothing
to the function and it can use its default as it would normally, but since
I had the good sense to ask an engine maintainer how they would approach
this problem, they cautioned me that this was the approach Stas took 10
years ago
https://github.com/php/php-src/compare/master...smalyshev:php-src:skip_params7,
that that approach was horrendous (paraphrasing) and they would never
support something like that (mainly because there's like 11 of them and
modifying them all would be a complete mess).
Interesting. Can you elaborate? There's like 11 of what?
Best,
Jakob
How exactly is this worrisome? Consider this:
class A {
public function __construct(private LogInterface $logger = new
DefaultLogger()) { }
}class ProxiedLogger implements LogInterface { ... }
$a = new A(new ProxyLogger(default));
If class A is programming to the
LogInterface
contract, the fact that it
gets a proxied version of the default should not matter in the least. Being
able to proxy like this means that a consumer of class A does not need to
know or care what the default implementation is; they can assume it follows
the same contract, and proxy to it regardless. The upstream developer
doesn't need to care, because they are programming to the interface, not
the implementation. This doesn't violate the separation of concerns
principle, nor covariance.
On a technicality, it doesn't violate any variance rules, because PHP doesn't treat constructors as substitutable; although rules of API stability between versions generally do.
For any other method, it would violate variance principles because they guarantee that the method accepts at least LogInterface, not exactly LogInterface. A child class can change the parameter type to any superset - a parent interface, a nullable type, a union, or mixed - and choose a default to match.
As Bruce Weirdan astutely pointed out [https://externals.io/message/125183#125274] the caller is treating the default value as though it's an output, which would need to be covariant (allows narrowing), but it's actually an input so is contravariant (allows widening).
If we state that default values, or their types, can reliably be inspected and used by callers in this way, we would logically need to declare any optional parameter invariant (must match exactly in all child classes), which would be a huge loss of functionality.
Regards,
Rowan Tommins
[IMSoP]
You'll likely identify the increased delay in such cases. Generally speaking these sorts of default values don't change a ton, but it's not unlikely that you may say "I'd like half that delay" or "twice that delay". But a better example would be introducing a backoff, which might look like this:
for ($i = 1; $i += 1 ; $i < 4) {
$result = call_some_client($url, default * $i);
if ($result->isSuccess()) {
break;
}
}
This could, I'm sure you agree, be easily done without access to the default timeout. In fact, the default timeout itself is entirely unnecessary to know... And I know you and I both know that most default timeouts are ridiculously oversized anyway (e.g. 30, 60 sec) for real-world production environments. :)
In other words, you know that you want it to use the default, and then allow an increasing timeout duration between calls if it fails. For this, I don't necessarily want or need to know what the default is, only that I want to do multiples of it in specific cases.
Is it a universally good idea? No. Does it have use cases? Yes.
Putting aside timeouts being a bad example here IMO because almost all default timeout values are unreasonably high, I simply don't think the use cases that are "good" come anywhere near the potential for abuse -- especially when it comes to default being an object or other complex type.
- decorating a default instance (e.g. to lazily create a proxy without knowing the default implementation used for an argument hinted against an interface)
This is exactly the usage I'm highlighted as problematic in my code example. You're introducing a new worry for the upstream API developer that doesn't need to exist, and violating a separation principle that has existed in PHP since default parameters were created 25+ years ago.
How exactly is this worrisome? Consider this:
class A {
public function __construct(private LogInterface $logger = new DefaultLogger()) { }
}
class ProxiedLogger implements LogInterface { ... }
$a = new A(new ProxyLogger(default));
If class A is programming to theLogInterface
contract, the fact that it gets a proxied version of the default should not matter in the least. Being able to proxy like this means that a consumer of class A does not need to know or care what the default implementation is; they can assume it follows the same contract, and proxy to it regardless. The upstream developer doesn't need to care, because they are programming to the interface, not the implementation. This doesn't violate the separation of concerns principle, nor covariance.
This example isn't worrisome. This one is (which I posted earlier in the thread). This patch creates a whole new BC issue for upstream APIs that callers use when they (ab)use default :
https://gist.github.com/coogle/7c0fbb750288ebdd1feb8a5e9185ba8c
In this Gist where before I didn't have to worry about changing the default value breaking downstream code (as long as the signature didn't change), I now do have to worry about changing the default value. This was an intentionally made example using strong typing, its even worse if the type is mixed.
If this feature is released with an overly broad scope in terms of expressions, etc. it's not like we can take it back at that point because now people are using it in unknown ways. It is not one I'm comfortable with a "let's move forward and see what happens" approach.
I didn't say that at all. I said we should identify the ones we absolutely know will be problematic, and restrict those from the outset. From there, we should identify the ones that might be problematic, and determine on a case by case basis if the risks outweigh the use cases before Bilge brings it to a vote.
The impression I've been getting from the conversations in this RFC is that there is no appetite for restrictions from Bilge or the supporters of the RFC. In fact Bilge said himself " there is no good reason, in my mind, to ever prohibit the expressiveness."
https://externals.io/message/125183#125217
So taking them at their word, it seems like that conversation is off the table. The RFC based on this thread is an up or down vote on full-fledged expressiveness, or nothing based on what I'm reading and I certainly won't be supporting that personally.
But if we lock it down too tightly from the outset, expanding it, while being possible, will more than likely mean an RFC for every expansion, because it's unlikely somebody will do anything comprehensive towards opening it up in the future. I'd rather not leave some of these use cases as a TBD for a later RFC, because that's very likely going to mean "never".
I DO think there are likely whole categories of expressions we can likely say "no" to - anything where the default represents a union type (and mixed is a union type) should likely only allow default by itself or as a bare value on the RHS of an expression.
I think you and I are saying the same thing here - the expressions need to be restricted. I 100% do think we would need to get into specifics as to which bucket (yes/no/maybe) though before I'd say we totally agree :) I can say I 100% agree that union types including mixed need to be out of this equation per my code example above. This RFC and the follow-on discussion has made explicitly clear, however, that's not a compromise the author(s) / advocate(s) are willing to entertain.
The argument against the feature that it expands the public API is puzzling to me, particularly when the only other solutions are (a) Reflection, or (b) named arguments. Named arguments are part of the public API, as the names themselves can change. Default values can change, but, importantly, a change in a default value does not change the actual signature of a function. Giving the ability to use
default
gives consumers of a function a far more stable API surface, particularly when they do not want to change a given default value, but do want to provide a more specific value for a later argument. Right now, if not using named arguments (e.g., because you're worried the argument name could change), your only other option is to use the Reflection API, which is more expensive, introduces a whole set of other possible runtime issues, and is far more convoluted to achieve.
I would argue the convolution is a feature, not a bug. The code example I provided in the gist above explains why I think it's an expansion of access into the API signature (specifically with union types). I of course concede named parameters also were an expansion of access into the API signature. These two things are not the same thing though in any other regard -- one had very clear wins and benefits (named parameters), this one feels more like syntax sugar that can be easily abused if left as a free-ranging expression.
Had this been only to allow the
default
keyword, I don't think we'd be having this discussion at all; I think it would be pretty self-evident that there's a need, and a lot of folks would be happy to sign on.
I agree.
But the author took it a step further, and asked, "What if ...?", and as such, the RFC provides additional benefits beyond giving you a keyword for using the default value, as it expands to allowing expressions. This gives a tremendous amount of flexibility and power, and solves some additional issues some of us have noticed.
and now there is a judgement call being made between these "additional benefits" (and I 100% concede there is an upside), and the costs. As presented IMO we've fallen far short of that being a winning proposition for PHP in part because it provides so much flexibility and power it starts feeling akin to introducing a new syntax to access private members of objects from outside those objects. Again, the issue being it is being proposed as a first-class citizen in terms of expression usage.
So I'd argue that what we need to weigh now is which of these expressions are truly benefits, which ones are side effects we can live with, and which will raise new problems we do not want to deal with.
But let's not just say "no expressions" - I think there's been some demonstrated value from a lot of these.
Honest to god I don't think I've read from one person to come out and say "no expressions". I've personally made a number of suggestions on expression limitations that may or may not be a good idea, such as:
https://externals.io/message/125183#125237
The problem is the RFC author has stated multiple times in their view that the expressions supported must be exhaustive and hasn't be willing to hear valid concerns to that.
Coogle
I DO think there are likely whole categories of expressions we can likely say "no" to - anything where the default represents a union type (and mixed is a union type) should likely only allow default by itself or as a bare value on the RHS of an expression.
I don't think having additional restrictions on union types makes much difference, because even if a signature doesn't use union types now, it can be extended to do so at any time, either in terms of inheritance or evolution in a new version.
It is a well understood principle that widening the type of an input cannot break existing uses, and relying on the concrete type of the default will remove that guarantee.
While the example of bitwise or in the RFC looks appealing, I am no longer sure I can even support that, because it has the same problem - in a future version of PHP, json_encode might change to take int|JsonOptions, with a default value that is an object not an int. Code passing an integer won't break, code accepting the default won't break, but code using "default | JSON_PRETTY_PRINT" will immediately get a TypeError. With the feature in place, such an evolution, which was previously safe, would become a hard BC break.
Forbidding that evolution, not just to that example, but to every function or method with any default parameter, is too high a price to pay for the benefit we'd get.
Regards,
Rowan Tommins
[IMSoP]
But let's not just say "no expressions" - I think there's been some
demonstrated value from a lot of these.Honest to god I don't think I've read from one person to come out and say
"no expressions".
Well...
I think it's causing more confusion than insight when we talk about blanket
"expressions" in the context of "default" here.
Expressions in function calls were always permitted:
foo(1+2); // Evaluating an expression and sending the result as the value
of the first parameter
The question is whether to allow OPERATIONS on "default".
Notice the difference here:
foo($userValue ?? default); // No operations performed on default, very
useful feature! This is still an expression involving the keyword default.
foo(default + 2); // "default" must evaluate to a value so we can perform
operations on it. This is also an expression, but I would not be in favor
of this behavior.
So the real 3 options are:
- No operations on "default". Treat "default" as a placeholder and send it
as such to the function. Any expression that evaluates to "default" is
fine. My opinion: Useful. - Allow any operations on "default". Extract the value and treat "default"
as any other "variable". My opinion: Dirty, possibly useful to some, but I
wouldn't use it, and would rather not find it in my code. - Allow SOME operations on "default", and block others. My opinion:
Nightmare. Both discussing which operations and why, documenting it and
maintaining it. I'm out :-)
So let's STOP talking about yes or no to EXPRESSIONS and START talking
about yes or no to OPERATIONS.
Best,
Jakob
On Mon, Aug 26, 2024 at 12:47 PM Rowan Tommins [IMSoP] imsop.php@rwec.co.uk
wrote:
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.
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 25/08/2024 14:35, Larry Garfield wrote:
The approach here seems reasonable overall. The mental model I have from the RFC is "yoink the default value out of the function, drop it into this expression embedded in the function call, and let the chips fall where they may." Is that about accurate? Yes, as it happens. That is the approach we took, because the alternative would have been changing how values are sent to functions, which would have required a lot more changes to the engine with no clear benefit. Internally it literally calls the reflection API, but a low-level call, that elides the class instantiation and unnecessary hoops of the public interface that would just slow it down. My main holdup is the need. I... can't recall ever having a situation where this is something I needed. Some of the examples show valid use cases (eg, the "default plus this binary flag" example), but again, I've never actually run into that myself in practice. That's fine. Not everyone will have such a need, and of those that do, I'm willing to bet it will be rare or uncommon at best. But for those times it is needed, the frequency by which it is needed in no way diminishes its usefulness. I rarely use
goto
but that doesn't mean we shouldn't have the feature. My other concern is the list of supported expression types. I understand how the implementation would naturally make all of those syntactically valid, but it seems many of them, if not most, are semantically nonsensical. Eg,default > 1
would take a presumably numeric default value and output a boolean, which should really never be type compatible with the function being called. (A param type of int|bool is a code smell at best, and a fatal waiting to happen at worst.) In practice, I think a majority of those expressions would be logically nonsensical, so I wonder if it would be better to only allow a few reasonable ones and block the others, to keep people from thinking nonsensical code would do something useful.
Since you're not the only one raising this, I will address it, but just
to say there is no good reason, in my mind, to ever prohibit the
expressiveness. To quote RobI'm reasonably certain you can write nonsensical PHP without this feature. I don't think we should be the nanny of developers.
See, I approach it from an entirely different philosophical perspective:
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, well, most good user interfaces. It's why it's good that US and EU power outlets are different, because they run different voltages, and blindly plugging one into the other can cause damage or death.
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.
This is the design philosophy behind password_hash()
and friends: The easy behavior is, 99% of the time, the right one, so doing the "right thing" is easy. Doing something dumb (like explicitly setting password_hash()
to use md5 or something) may be possible, but it requires extra work to be dumb.
Good design makes the easy path the safe path.
Now, certainly, the definition of "stupid things" is subjective and squishy, and reasonable people can disagree on where that threshold is. That's what a robust discussion is for, to figure out what qualifies as a "stupid thing" in this case.
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.
As you've noted, this is already applicable only in some edge cases to begin with, so enabling edge cases of edge cases that only maybe make sense if you squint is very likely in the "stupid things" territory.
I fully agree with that sentiment. It seems to be biting me that I went
to the trouble of listing out every permutation of what expression
means where perhaps this criticism would not have been levied at all
had I chosen not to do so.
From one RFC author to another, it's better to make that list explicitly and let us collectively think through the logic of it than to be light on details and not realize what will break until later. We've had RFCs that did that, and it caused problems. The discussion can absolutely be frustrating (boy do I know), but the language is better for it. So I'm glad you did call it out so we could have this discussion.
--Larry Garfield
My other concern is the list of supported expression types. I
understand how the implementation would naturally make all of those
syntactically valid, but it seems many of them, if not most, are
semantically nonsensical.
I tend to agree with Larry and John that the list of operators should be
restricted - we can always allow more in future, but restricting later
is much harder.
A few rules that seem logical to me:
- The expression should be reasonably guaranteed to produce the same
type as the actual default.
- No casts
- No comparison operators, because they produce booleans from
non-boolean input - No "<=>". Technically, it has an integer result, but it's rare to use
it as one, rather than a kind of three-value boolean - No "instanceof"
- No "empty"
- The expression should not have side effects (outside of exotic
operator overloads).
- No "include", "require", etc
- No "throw"
- No "print"
- Borderline, but I would also say no "clone"
- The expression should be passing additional information into the
function, not pulling information out of it. The syntax shouldn't be a
way to write obfuscated reflection, or invert data flow from callee to
caller.
- No assignments.
- No ternaries with "default" on the left-hand side - "$foo ? $bar :
default" is acting on local knowledge, but "default ? $foo : $bar" is
acting on information the caller shouldn't know - Same for "?:" and "??"
- No "match" with "default" as the condition or branch, for the same
reason. "match($foo) { $bar => default }" is fine, match(default) { ...
}" or "match($foo) { default => ... }" are not.
Note that these can be seen as aspects of the same rule: the aim of the
expression should be to transform the default value into another value
of the same type, not to pull it out and perform arbitrary operations
based on it.
I believe that leaves us with:
- Arithmetic operators: binary + - * / % **, unary + -
- Bitwise operators: & | ^ << >> ~
- Boolean operators: && || and or xor !
- Conditions with default on the RHS: $foo ? $bar : default, $foo ?:
default, $foo ?? default, match($foo) { $bar => default } - Parentheses: (((default)))
Even then, I look at that list and see more problems than use cases. As
the RFC points out, library authors already worry about the maintenance
burden of named argument support, will they now also need to question
whether someone is relying on "default + 1" having some specific effect?
Maybe we should instead require justification for each addition:
- Bitwise | is nicely demonstrated in the RFC
- Bitwise & could probably be justified on similar grounds
- "$foo ? $bar : default" is discussed in the RFC
- The other "conditions with default on the RHS" in my shortlist above
fit the same basic use case
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.
Regards,
--
Rowan Tommins
[IMSoP]
My other concern is the list of supported expression types. I
understand how the implementation would naturally make all of those
syntactically valid, but it seems many of them, if not most, are
semantically nonsensical.I tend to agree with Larry and John that the list of operators should be restricted - we can always allow more in future, but restricting later is much harder.
A few rules that seem logical to me:
- The expression should be reasonably guaranteed to produce the same type as the actual default.
- No casts
- No comparison operators, because they produce booleans from non-boolean input
- No "<=>". Technically, it has an integer result, but it's rare to use it as one, rather than a kind of three-value boolean
- No "instanceof"
- No "empty"
- The expression should not have side effects (outside of exotic operator overloads).
- No "include", "require", etc
- No "throw"
- No "print"
- Borderline, but I would also say no "clone"
- The expression should be passing additional information into the function, not pulling information out of it. The syntax shouldn't be a way to write obfuscated reflection, or invert data flow from callee to caller.
- No assignments.
- No ternaries with "default" on the left-hand side - "$foo ? $bar : default" is acting on local knowledge, but "default ? $foo : $bar" is acting on information the caller shouldn't know
- Same for "?:" and "??"
- No "match" with "default" as the condition or branch, for the same reason. "match($foo) { $bar => default }" is fine, match(default) { ... }" or "match($foo) { default => ... }" are not.
Note that these can be seen as aspects of the same rule: the aim of the expression should be to transform the default value into another value of the same type, not to pull it out and perform arbitrary operations based on it.
I believe that leaves us with:
- Arithmetic operators: binary + - * / % **, unary + -
- Bitwise operators: & | ^ << >> ~
- Boolean operators: && || and or xor !
- Conditions with default on the RHS: $foo ? $bar : default, $foo ?: default, $foo ?? default, match($foo) { $bar => default }
- Parentheses: (((default)))
Even then, I look at that list and see more problems than use cases. As the RFC points out, library authors already worry about the maintenance burden of named argument support, will they now also need to question whether someone is relying on "default + 1" having some specific effect?
Maybe we should instead require justification for each addition:
- Bitwise | is nicely demonstrated in the RFC
- Bitwise & could probably be justified on similar grounds
- "$foo ? $bar : default" is discussed in the RFC
- The other "conditions with default on the RHS" in my shortlist above fit the same basic use case
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.
Regards,
--
Rowan Tommins
[IMSoP]
Hi Rowan, you went through a lot of trouble to write this out, and the reasoning makes sense to me. However, all the nonsensical things you say shouldn’t be allowed are already perfectly allowed today, you just have to type a bunch of boilerplate reflection code. There is no new behavior here, just new syntax.
— Rob
Hi Rowan, you went through a lot of trouble to write this out, and the
reasoning makes sense to me. However, all the nonsensical things you
say shouldn’t be allowed are already perfectly allowed today, you just
have to type a bunch of boilerplate reflection code. There is no new
behavior here, just new syntax.
Firstly, your response to John was essentially "please give more
details" [https://externals.io/message/125183#125214], and your response
to me is "thanks for the details, but I'm not going to engage with
them". That's a bit frustrating.
Secondly, I don't think "it's possible with half a dozen lines of
reflection, so it's fine for it to be a first-class feature of the
language syntax" is a strong argument. 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.
Regards,
--
Rowan Tommins
[IMSoP]
Hi Rowan, you went through a lot of trouble to write this out, and the
reasoning makes sense to me. However, all the nonsensical things you
say shouldn’t be allowed are already perfectly allowed today, you just
have to type a bunch of boilerplate reflection code. There is no new
behavior here, just new syntax.Firstly, your response to John was essentially "please give more
details" [https://externals.io/message/125183#125214], and your response
to me is "thanks for the details, but I'm not going to engage with
them". That's a bit frustrating.
Oh, my apologies! That wasn’t my intention! With John and yourself, I do agree with you. I’m just trying to understand the logic in limiting it. As in, “I intuitively feel the same way but I don’t know why but maybe you do.” Intuition sucks sometimes.
Secondly, I don't think "it's possible with half a dozen lines of
reflection, so it's fine for it to be a first-class feature of the
language syntax" is a strong argument. 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.Regards,
--
Rowan Tommins
[IMSoP]
That makes sense, but is it uncommon because it is hard and slow, or because it is genuinely not a common need?
— Rob
Even then, I look at that list and see more problems than use cases. As the RFC points out, library authors already worry about the maintenance burden of named argument support, will they now also need to question whether someone is relying on "default + 1" having some specific effect?
Maybe we should instead require justification for each addition:
- Bitwise | is nicely demonstrated in the RFC
- Bitwise & could probably be justified on similar grounds
- "$foo ? $bar : default" is discussed in the RFC
- The other "conditions with default on the RHS" in my shortlist above fit the same basic use case
IMO the operations that make sense in this context are:
- Some Bitwise operators: & | ^
- Conditions with default on the RHS: $foo ? $bar : default, $foo ?: default, $foo ?? default, match($foo) { $bar => default }
- Parentheses: (((default)))
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.
I 100% agree.
G((default)->F()); // lol
Special-casing theT_DEFAULT
grammar would not only bloat the grammar rules but also increase the chance that new expression grammars introduced in future, which could conveniently interoperate withdefault
, would be unintentionally excluded by omission.
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. It should not become a fast easy way to inject a new potentially complex dependency which is what allowing a full expression support would allow.
The fact that Reflection allows me to pull out a private member doesn't mean accessing private members of objects should be given its own language syntax.
Frankly, not only should the op list be limited but ideally it should also only be valid based on the type of the upstream API call (e.g. bitwise operators should only be valid if the upstream API call has a type int ).
Special-casing the
T_DEFAULT
grammar would not only bloat the grammar rules but also increase the chance that new expression grammars introduced in future, which could conveniently interoperate withdefault
, would be unintentionally excluded by omission.
Forgot to add that I don't think the fact doing this properly requires a more complex grammar is a strong argument for doing it "the easy way" of allowing all expressions. It's a special case, and that should be reflected in the grammar.
Hi Rowan,
- The expression should be passing additional information into the function, not pulling information out of it. The syntax shouldn't be a way to write obfuscated reflection, or invert data flow from callee to caller.
- No assignments.
- No ternaries with "default" on the left-hand side - "$foo ? $bar : default" is acting on local knowledge, but "default ? $foo : $bar" is acting on information the caller shouldn't know
- Same for "?:" and "??"
- No "match" with "default" as the condition or branch, for the same reason. "match($foo) { $bar => default }" is fine, match(default) { ... }" or "match($foo) { default => ... }" are not.
I think this brings up a good question on what exactly should be intended to be public API. Currently, there's two main ways to effectively write a default parameter:
function foo(int $param = 42) {}
function bar(?int $param) { $param ??= 42; }
In the former, the default value is listed in the function declaration, along with the function name, and parameter type and name, which are already part of the public interface.
In the latter, the default value is an implementation detail of the function, and is not part of the function declaration.
(You could also ?int $param = 42, but I'd argue that at that point, if you really need to distinguish among the set of (unspecified, null, value), you're better off with an ADT, which we don't have yet. And you can also explicitly expose a default value as a static value on a type, when a function itself doesn't want to/shouldn't make the policy decision of what the default is.)
Although I'm not sold on the idea of using default as part of an expression, I would argue that a default function parameter value is fair game to be read and manipulated by callers. If the default value was intended to be private, it shouldn't be in the function declaration.
One important case where reading the default value could be important is in interoperability with different library versions. For example, a library might change a default parameter value between versions. If you're using the library, and want to support both versions, you might both not want to set the value, and yet also care what the default value is from the standpoint of knowing what to expect out of the function.
-John
Although I'm not sold on the idea of using default as part of an
expression, I would argue that a default function parameter value is
fair game to be read and manipulated by callers. If the default value
was intended to be private, it shouldn't be in the function declaration.
There's an easy argument against this interpretation: child classes can
freely change the default value for a parameter, as long as they do not
make it mandatory. https://3v4l.org/SEsRm
That matches my intuition: that the public API, as a contract, states
that the parameter is optional; the specification of what happens when
it is not provided is an implementation detail.
For comparison, consider constructor property promotion; the caller
shouldn't know or care whether a class is defined as:
public function __construct(private int $bar) {}
or:
private int $my_bar;
public function __construct(int $bar) { $this->my_bar = $bar; }
The syntax sits in the function signature because it's convenient, not
because it's part of the API.
One important case where reading the default value could be important is
in interoperability with different library versions. For example, a
library might change a default parameter value between versions. If
you're using the library, and want to support both versions, you might
both not want to set the value, and yet also care what the default value
is from the standpoint of knowing what to expect out of the function.
This seems contradictory to me. If you use the default, you're telling
the library that you don't care about that parameter, and trust it to
provide a default.
If you want to know what the library did with its arguments, reflecting
the signature will never be enough anyway. For example, it's quite
common to write code like this:
function foo(?SomethingInterface $blah = null) {
if ( $blah === null ) {
$blah = self::_setup_default_blah();
}
// ...
}
A caller can't tell by looking at the signature that a new version of
the library has changed what _setup_default_blah() returns. If the
library doesn't provide an API to get $blah out later, then it's a
private detail that the caller has no business inspecting.
Regards,
--
Rowan Tommins
[IMSoP]
Although I'm not sold on the idea of using default as part of an
expression, I would argue that a default function parameter value is
fair game to be read and manipulated by callers. If the default value
was intended to be private, it shouldn't be in the function declaration.There's an easy argument against this interpretation: child classes can freely change the default value for a parameter, as long as they do not make it mandatory. https://3v4l.org/SEsRm
That matches my intuition: that the public API, as a contract, states that the parameter is optional; the specification of what happens when it is not provided is an implementation detail.
For comparison, consider constructor property promotion; the caller shouldn't know or care whether a class is defined as:
public function __construct(private int $bar) {}
or:
private int $my_bar;
public function __construct(int $bar) { $this->my_bar = $bar; }The syntax sits in the function signature because it's convenient, not because it's part of the API.
One important case where reading the default value could be important is
in interoperability with different library versions. For example, a
library might change a default parameter value between versions. If
you're using the library, and want to support both versions, you might
both not want to set the value, and yet also care what the default value
is from the standpoint of knowing what to expect out of the function.This seems contradictory to me. If you use the default, you're telling the library that you don't care about that parameter, and trust it to provide a default.
If you want to know what the library did with its arguments, reflecting the signature will never be enough anyway. For example, it's quite common to write code like this:
function foo(?SomethingInterface $blah = null) {
if ( $blah === null ) {
$blah = self::_setup_default_blah();
}
// ...
}A caller can't tell by looking at the signature that a new version of the library has changed what _setup_default_blah() returns. If the library doesn't provide an API to get $blah out later, then it's a private detail that the caller has no business inspecting.
Regards,
--
Rowan Tommins
[IMSoP]
I think you've hit an interesting point here, but probably not what you intended.
For example, let's consider this function:
json_encode(mixed $value, int $flags = 0, int $depth = 512): string|false
Already, you have to look up the default value of depth or set it to something that makes sense, as well as $flags. So you do this:
json_encode($value, JSON_THROW_ON_ERROR, 512);
You are doing this even when you omit the default. If you set it to a variable to spell it out:
$default_flags = 0 | JSON_THROW_ON_ERROR;
$default_depth = 512; // according to docs on DATE
json_encode($value, $default_flags, $default_depth);
Can now be rewritten:
json_encode($value, $default_flags = default | JSON_THROW_ON_ERROR, $default_depth = default);
This isn't just reflection, this is saving me from having to look up the docs/implementation and hardcode values. The implementation is free to change them, and my code will "just work."
Now, let's look at a more non-trivial case from some real-life use-cases, in the form of a plausible story:
public function __construct(
private LoggerInterface|null $logger = null,
private string|null $name = null,
Level|null $level = null,
)
This code constructs a new logger composed from an already existing logger. When constructing it, I may look up what the default values are and decide if I want to override them or not. Otherwise, I will leave it as null.
A coworker and I got to talking about this interface. It kind of sucks, and we don't like it. It's been around for ages, so we are worried about changing it. Specifically, we are wondering if we should use SuperNullLogger as the default instead of null (which happens to just create a NullLogger a few lines later). We are pretty sure making this change won't cause any issues, but to be extra safe, we will do it only on a single code path; further, we are 100% sure we are going to change this signature, so we need to do it in a forward-compatible way. Thus, we will set it to SuperNullLogger if-and-only-if the default value is null:
default ?? new SuperNullLogger()
Now, we can run this in production and see how well it performs. Incidentally, we discover that NullLogger implementation is superior and we can now change the default:
public function __construct(
private LoggerInterface $logger = new NullLogger(),
private string|null $name = null,
Level|null $level = null,
)
That one code path "magically" updates as soon as the library is updated, without having to make further changes. Anything that is hardcoded "null" will break in tests/static analysis, making it easy to locate. Further, we can test other types of NullLoggers just as easily:
default instanceof NullLogger ? new BasicNullLogger() : default
So, yes, I think in isolation the feature might look strange, and some operations might look nonsensical, but I believe there is a use case here that was previously rather hard to do; or statically done via someone looking up some documentation/code and doing a search-and-replace.
— Rob
Although I'm not sold on the idea of using default as part of an
expression, I would argue that a default function parameter value is
fair game to be read and manipulated by callers. If the default value
was intended to be private, it shouldn't be in the function declaration.There's an easy argument against this interpretation: child classes can freely change the default value for a parameter, as long as they do not make it mandatory. https://3v4l.org/SEsRm
That matches my intuition: that the public API, as a contract, states that the parameter is optional; the specification of what happens when it is not provided is an implementation detail.
For comparison, consider constructor property promotion; the caller shouldn't know or care whether a class is defined as:
public function __construct(private int $bar) {}
or:
private int $my_bar;
public function __construct(int $bar) { $this->my_bar = $bar; }
The syntax sits in the function signature because it's convenient, not because it's part of the API.
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. There's no reason default values couldn't (or shouldn't) become part of the API contract in the same way.
(Note that in some other languages, default parameter values are not only part of the API contract, but they're emitted into the clients when compiled, so an API can change/add/remove its default values and the client continues to function as it used to with the value as defined at compile time. This doesn't currently matter for PHP, where you have the full source to anything you run, but could become important later if PHP gained ahead-of-time compiled binary modules.)
One important case where reading the default value could be important is
in interoperability with different library versions. For example, a
library might change a default parameter value between versions. If
you're using the library, and want to support both versions, you might
both not want to set the value, and yet also care what the default value
is from the standpoint of knowing what to expect out of the function.This seems contradictory to me. If you use the default, you're telling the library that you don't care about that parameter, and trust it to provide a default.
If you want to know what the library did with its arguments, reflecting the signature will never be enough anyway. For example, it's quite common to write code like this:
function foo(?SomethingInterface $blah = null) {
if ( $blah === null ) {
$blah = self::_setup_default_blah();
}
// ...
}
A caller can't tell by looking at the signature that a new version of the library has changed what _setup_default_blah() returns. If the library doesn't provide an API to get $blah out later, then it's a private detail that the caller has no business inspecting.
Well, but that's the private default example I described. In that case you're not intended to be able to reason about what the default is, because it's a private implementation detail of the function, as opposed to being expressed in the parameter list. Although, if it weren't intended to be an implementation detail, the only thing stopping you from writing in the parameter list like this:
function foo(SomethingInterface $blah = self::_setup_default_blah()) {...}
is because PHP doesn't currently allow default values to be computed at runtime. (Maybe it should.)
-John
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]
Hi gang,
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.
Seems like you are missing an option for your theme example, which would be to simply extend the Config class?
While I can see the value in the concept of default , I think it's a mistake to allow default to be used as a generic operand as shown in the RFC appendix. It seems to me that the whole point of something like default is to not have to worry about what the upstream API wanted for that value, but the second you start allowing operations where default is an operand you've reintroduced the problem you were trying to avoid if the upstream API were to change the type of what default ultimately resolves to. Worse actually because now I have no idea what default is when I read code without having to dig up the upstream API.
Other thoughts here are what happens when default resolves to an object or enumeration or something complex? Your original example had CuteTheme , so can you call a method of default ?? I could entirely see someone doing something like this for example:
enum Foo:string {
// cases
public function buildSomeValidBasedOnCase(): int { // ... }
}
F(MyClass::makeBasedOnValue(default->buildSomeValidBasedOnCase()))
IMO most operators listed in the appendix should be disallowed. I can see the value of default | JSON_PRETTY_PRINT, but I am pretty strongly opposed to the idea of introducing a "conditional based on the default value of an upstream API call" concept of default >=1 .
Hi gang,
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.Seems like you are missing an option for your theme example, which would
be to simply extend the Config class?While I can see the value in the concept of default , I think it's a
mistake to allow default to be used as a generic operand as shown in the
RFC appendix. It seems to me that the whole point of something like
default is to not have to worry about what the upstream API wanted for
that value, but the second you start allowing operations where default
is an operand you've reintroduced the problem you were trying to avoid if
the upstream API were to change the type of what default ultimately
resolves to. Worse actually because now I have no idea what default is
when I read code without having to dig up the upstream API.
If the underlying API changes the argument type, consumers will have an
issue regardless. For those cases where the expression is simply default
,
you'd actually be protected from the API change, which is a net benefit
already.
This also protects the user from changes in the argument names.
Other thoughts here are what happens when default resolves to an object
or enumeration or something complex? Your original example had CuteTheme ,
so can you call a method of default ?? I could entirely see someone doing
something like this for example:enum Foo:string {
// casespublic function buildSomeValidBasedOnCase(): int { // ... }
}
F(MyClass::makeBasedOnValue(default->buildSomeValidBasedOnCase()))
IMO most operators listed in the appendix should be disallowed. I can see
the value of default | JSON_PRETTY_PRINT, but I am pretty strongly
opposed to the idea of introducing a "conditional based on the default
value of an upstream API call" concept of default >=1 .
If the underlying API changes the argument type, consumers will have an issue regardless. For those cases where the expression is simply
default
, you'd actually be protected from the API change, which is a net benefit already.This also protects the user from changes in the argument names.
As I said, I don't have a particular problem with default as a keyword to express "whatever the default value might be in the function declaration", but I do have some real concerns about its use as an operand in an expression. The RFC provides for a single valid use case of operators (i.e. things like default | JSON_PRETTY_PRINT
), yet calls for a huge array of valid operations, many of which the RFC itself notes don't make much / any sense. I'd personally like to see this RFC dramatically reduce the scope of operations supported with default as an operand initially (e.g. perhaps only bitwise ops), and revisit additional operations as needed down the road. IMO there is a very small subset of all PHP operators that make any sense at all in this context, and even fewer that I think are a good idea to allow even if they might make some sort of sense.
If the underlying API changes the argument type, consumers will have an issue regardless. For those cases where the expression is simply
default
, you'd actually be protected from the API change, which is a net benefit already.This also protects the user from changes in the argument names.
As I said, I don't have a particular problem with
default
as a keyword to express "whatever the default value might be in the function declaration", but I do have some real concerns about its use as an operand in an expression. The RFC provides for a single valid use case of operators (i.e. things likedefault | JSON_PRETTY_PRINT
), yet calls for a huge array of valid operations, many of which the RFC itself notes don't make much / any sense. I'd personally like to see this RFC dramatically reduce the scope of operations supported withdefault
as an operand initially (e.g. perhaps only bitwise ops), and revisit additional operations as needed down the road. IMO there is a very small subset of all PHP operators that make any sense at all in this context, and even fewer that I think are a good idea to allow even if they might make some sort of sense.
Which operants don’t make sense?
— Rob
Which operants don’t make sense?
Well certainly all of the ones toward the end of the appendix in the RFC the RFC itself notes are non-sensical. Personally, I'm not sold on the idea default should be an operand in an expression at all though.
I do see the value of bitwise operators / expressions as highlighted in the RFC. There might be other cases, but I'm wary of them -- I don't think giving developers the ability to write logic and expressions against hard-coded default values in upstream APIs has a lot of merit in most cases. I can tell you I doubt I would ever support the idea of calling methods of default , which I'm still unclear if this RFC is proposing.
I would suggest a compromise where the RFC be refocused to those expressions and operators that explicitly make sense, and leave the rest of them out for now. To me, that basically means "default with bitwise expression support" based on what I've seen so far.
John
Other thoughts here are what happens when |default| resolves to an
object or enumeration or something complex? Your original example had
|CuteTheme| , so can you call a method of |default| ?? I could
entirely see someone doing something like this for example:enum Foo:string {
// casespublic function buildSomeValidBasedOnCase(): int { // ... }
}F(MyClass::makeBasedOnValue(default->buildSomeValidBasedOnCase()))
As you have written it, no, you will get a parser error: Parse error:
syntax error, unexpected token "->", expecting ")"
However, you can wrap the default
in parens as in the following example:
class C {
function F() {
echo 'lol';
}
}
function G($V = new C) {}
G((default)->F()); // lol
Hi gang,
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.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.
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
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 thestring
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.
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.
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:
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 thestring
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 |
+---------------------------------------------------------------------+
| https://hei.gl/appointmentwithandreas |
+---------------------------------------------------------------------+
| GPG-Key: https://hei.gl/keyandreasheiglorg |
+---------------------------------------------------------------------+
Hey folks.
Am 26.08.24 um 11:26 schrieb Bilge:
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 thestring
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,
BilgeI think I am missing something here. From my understanding we are
either coding against the interface and then it should not be
possible to usedefault
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 providingdefault
when no default is set in
the concrete implementation of the function signature raises a fatal
error.
Hi Andreas,
Thanks so much for pointing this out. Your argument seems absolutely
correct to me, that this is an invalid polymorphic pattern, and as such,
I have stopped development of this feature. I still want to thank the
other Andreas (L) for raising it, and intend to add it to the possible
future scope in the RFC to provide an exception to permit default
on
the LHS of null-coalesce. We did at least assess that such an exception
would be viable even if the development work would be disproportional to
the benefit, so it's worth noting that down.
Cheers,
Bilge
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]
Hey Bilge,
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);
}
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
Hey Bilge,
Hi :)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.
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.One question (sorry if someone already asked, I scanned the thread but
it is getting long..):
I don't blame you. I'll summarise the main takeaways in the RFC later.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
Hey Jordi,
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
Hi gang,
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.
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
Hi,
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.
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.
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 toF
?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 default
s 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
Hi gang,
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.
I liked this up to the point where I saw:
$f = fn ($v = 1, $default = 2) => $v + $default;
var_dump($f(default: default + 1)); // int(4)
Using 'default' as a place holder for (not) passing an argument seems
useful. I am however much uncertain about using composition with the
keyword in expressions.
I also think that implementation would probably be significantly less
complex as it was only used for placeholders. It's likely something that
can be handled in the parser.
Having an opcode for it, that does internal reflection, is what I'm
unsure about.
cheers,
Derick
Using 'default' as a place holder for (not) passing an argument seems
useful. I am however much uncertain about using composition with the
keyword in expressions.
Presumably you mean ternary and null coalesce would still be acceptable?
Otherwise, it is little more than an alternative to named parameters,
which is seldom useful. Ternary and null coalesce are, themselves,
expressions so I expect that is why I was guided towards treating
default
the same way. Whilst the applications beyond use in
conditionals still strikes me as limited, it also appears equally
harmless. Though the number of people whom feel its use in expression
should be limited is steadily growing, I'm still waiting on someone to
publish an exclusion list with justification for each exclusion, because
again, I think a plurality of possibilities are harmless. Nevertheless,
an inclusion list was produced by IMSoP, based on what he felt would be
useful, but I think arbitrary exclusions are unintuitive for developers
and likely to limit someone from doing something justifiably useful that
we didn't conceive at the time.
I also think that implementation would probably be significantly less
complex as it was only used for placeholders.
Would it? I was told that the alternative was to modify all the SEND
opcodes, which is anything but less complex. The current implementation
seems necessarily simple to me since it just adds a single new opcode
that doesn't interfere with anything else. I say necessarily, because if
it was actually complex, I probably couldn't have written the solution
given my limited ability.
It's likely something that can be handled in the parser.
Clearly this problem cannot be solved solely in the parser. If I assume
you're proposing to just allow default
in conditionals, what is
responsible for compiling that and how will the called function know
what to do with it?
Cheers,
Bilge
Whilst the applications beyond use in conditionals still strikes me as limited, it also appears equally harmless. Though the number of people whom feel its use in expression should be limited is steadily growing, I'm still waiting on someone to publish an exclusion list with justification for each exclusion, because again, I think a plurality of possibilities are harmless.
I'm slightly baffled why you're still looking at it from this angle. Possibly, the volume of messages in the thread has made it easy to miss some of the points that have been raised.
There is a fundamental, unavoidable, cost to allowing any expression which relies on the type or value of the parameter's default not changing in subclasses or future versions. It makes changes that are currently guaranteed safe by the principles of the language, into compatibility breaks. As Bruce pointed out, it introduces a contravariant output, contrary to the substitution principle, unless we break a bunch of existing use cases by declaring parameter defaults invariant.
You're not going to see any kind of "exclusion list"of operators, because on closer examination of the impact, we've realised that it's not particular operators that are the problem, it's the entire principle of allowing "default" to be evaluated to a value in the middle of an expression.
The only expressions that are in some sense "safe" are those that can apply equally to any possible type that the function could in future set as the default. In theory, that includes a match statement with an arm of "default => default", e.g.
json_encode($data, default, match(gettype(default)) { 'int' => default | JSON_PRETTY_PRINT, default => default });
Apart from being incredibly hard to read, that's not even useful: the aim is to always enable pretty printing, but the result is "enable pretty print, unless the type of the default happens to change".
So that leaves us with those expressions where "default" is only a result, not an input:
expression ?: default
expression ? expression : default
expression ? default : expression
expression ?? default
Unless I've forgotten something, that's it; that's your list of allowed expressions.
Whether that's possible to enforce, in the parser, the compiler, or the executor, I don't know. But if it's not, my opinion is that the entire feature has an unanticipated problem that makes it unworkable. It would be a shame, because on the face of it I can see the value, but sometimes you just hit a dead end and have to turn back.
Regards,
Rowan Tommins
[IMSoP]
The only expressions that are in some sense "safe" are those that can apply equally to any possible type that the function could in future set as the default. In theory, that includes a match statement with an arm of "default => default", e.g.
json_encode($data, default, match(gettype(default)) { 'int' => default | JSON_PRETTY_PRINT, default => default });
Apart from being incredibly hard to read, that's not even useful: the aim is to always enable pretty printing, but the result is "enable pretty print, unless the type of the default happens to change".
I'm not sure this could even work at all. The "default" parameter to gettype()
isn't the default value of the third parameter to json_encode()
. It's the default value of the first parameter to gettype()
. Which would probably fail, since gettype()
's first parameter doesn't have a default. I suppose this could be solved by specifying an offset or label (e.g. as with continue 2
in a nested loop), but that would just make it even harder to read.
-John
I'm not sure this could even work at all. The "default" parameter to
gettype()
isn't the default value of the third parameter to
json_encode()
. It's the default value of the first parameter to
gettype()
. Which would probably fail, sincegettype()
's first parameter
doesn't have a default. I suppose this could be solved by specifying an
offset or label (e.g. as withcontinue 2
in a nested loop), but that
would just make it even harder to read.
Ah, good catch. So without a pattern-matching "default is int", I'm not
sure how you'd even achieve that safety.
There are a few other examples on this thread that contain the same
mistake, such as MWOP's:
class A {
public function __construct(private LogInterface $logger = new
DefaultLogger()) { }
}
class ProxiedLogger implements LogInterface { ... }
$a = new A(new ProxyLogger(default));
The "default" wouldn't look anything up in A::__construct, only in
ProxyLogger::__construct. To pass the default out to any kind of
function, you'd have to write some contorted expression like this:
$a = new A( $default=default && false ?: new ProxyLogger($default) );
That's even further into Obfuscated Code Contest territory than "default
=> default", and further reduces the reasonable use cases for expressions.
--
Rowan Tommins
[IMSoP]
Hi gang,
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.
Now the dust has settled, I've updated the RFC to version 1.1. The
premise of the RFC is unchanged, but the proposal has been expanded and
a discussion section added to summarise the ~100 message thread to
capture the major concerns raised in a condensed format. I hope I've
done a good job of fairly and accurately representing your concerns, but
if not please correct me.
Furthermore, a secondary vote has been added. The secondary vote will be
open to all (whether in favour or against the proposal) to capture
alternative implementations you might also be in favour of. If the
primary vote passes, the secondary vote won't matter, but otherwise it
may help guide our sails in future.
Kind regards,
Bilge
One thought re-reading the RFC.
abstract class Theme {
public function bar();
}
class CuteTheme extends Theme {
public function foo();
}
class Config {
public function __construct(Theme $theme = new CuteTheme()) {}
}
$a = new Config(default->foo());
In the proposed (updated) RFC would this be proposed to work? If so this should be added to the discussion section as something I think is equally as problematic as union types. I don't think saying "Union and Mixed" is broad enough. In this case it would actually have to only allow Theme (whatever that was) and prevent you from calling foo() because that isn't a member of Theme . Otherwise it's the same problem as union types in a different color.
Coogle
Hi gang,
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.Now the dust has settled, I've updated the RFC to version 1.1. The
premise of the RFC is unchanged, but the proposal has been expanded and
a discussion section added to summarise the ~100 message thread to
capture the major concerns raised in a condensed format. I hope I've
done a good job of fairly and accurately representing your concerns, but
if not please correct me.Furthermore, a secondary vote has been added. The secondary vote will be
open to all (whether in favour or against the proposal) to capture
alternative implementations you might also be in favour of. If the
primary vote passes, the secondary vote won't matter, but otherwise it
may help guide our sails in future.Kind regards,
Bilge
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.Now the dust has settled, I've updated the RFC to version 1.1. The
premise of the RFC is unchanged, but the proposal has been expanded and
a discussion section added to summarise the ~100 message thread to
capture the major concerns raised in a condensed format. I hope I've
done a good job of fairly and accurately representing your concerns, but
if not please correct me.
Hi,
I will try to find some time over the next few days to write something up, because there are some clear misunderstandings in that current text.
- It's not about union types. There are lots of examples using union types, because they're easier to illustrate than class/interface hierarchies, but "disallowing default to be passed to union types" wouldn't even help with the example directly below that sentence.
- The paragraph about "Default as a contract" misses the point by a mile. There's a world of difference between "the new version of this library has different behaviour" and "the new version of this library gives a TypeError in my code which used to work".
- It's not about "critics" and "preferred versions", or it doesn't need to be. We can say "here are some situations where it might be useful; and here are some situations where it would cause unexpected errors; do we think the benefits of one outweigh the cost/risk of the other?"
I already shared this with you, but for the benefit of the wider audience, here are some examples I've put together of when users might vary the types of defaults, and how that would cause errors with the proposed feature: https://gist.github.com/IMSoP/16e2422d86e3ab513d6b0658009d0c06 I stress again, I'm not trying to win points in a popularity contest here, I'm trying to make sure we properly lay out the pros and cons of the proposal.
It's worth noting that the drawback being pointed out is similar to one encountered with named arguments. Nikita dedicated a substantial part of that RFC to discussing the problem, its possible solutions, and the impact of the proposed direction: this section https://wiki.php.net/rfc/named_params#parameter_name_changes_during_inheritance and this one https://wiki.php.net/rfc/named_params#to_parameter_name_changes_during_inheritance and most of this one https://wiki.php.net/rfc/named_params#backwards_incompatible_changes
Regards,
Rowan Tommins
[IMSoP]
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.Now the dust has settled, I've updated the RFC to version 1.1. The
premise of the RFC is unchanged, but the proposal has been expanded
and a discussion section added to summarise the ~100 message thread to
capture the major concerns raised in a condensed format. I hope I've
done a good job of fairly and accurately representing your concerns,
but if not please correct me.
As promised, I have written up a full explanation of the type safety
issues here: https://wiki.php.net/rfc/default_expression/type_safety
I have tried to write this as a neutral description of the problem and
the possible approaches we could take, to be inserted directly into the
current RFC, rather than as a counter-opinion or a narrative of who said
what.
I have included the 4 options which I believe are the only ones we have;
it is then a matter of opinion which we think is best. For the record,
my opinion remains that option 3 (limit to conditional expressions) is
preferable, but I have assumed the RFC will continue to advocate for
option 1 (allow any expression and assume problems will be rare).
I hope I have explained it clearly enough this time to overcome the
previous misunderstandings of where the issue lies.
Regards,
--
Rowan Tommins
[IMSoP]
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.Now the dust has settled, I've updated the RFC to version 1.1. The
premise of the RFC is unchanged, but the proposal has been expanded
and a discussion section added to summarise the ~100 message thread to
capture the major concerns raised in a condensed format. I hope I've
done a good job of fairly and accurately representing your concerns,
but if not please correct me.As promised, I have written up a full explanation of the type safety
issues here: https://wiki.php.net/rfc/default_expression/type_safetyI have tried to write this as a neutral description of the problem and
the possible approaches we could take, to be inserted directly into the
current RFC, rather than as a counter-opinion or a narrative of who said
what.I have included the 4 options which I believe are the only ones we have;
it is then a matter of opinion which we think is best. For the record,
my opinion remains that option 3 (limit to conditional expressions) is
preferable, but I have assumed the RFC will continue to advocate for
option 1 (allow any expression and assume problems will be rare).I hope I have explained it clearly enough this time to overcome the
previous misunderstandings of where the issue lies.Regards,
--
Rowan Tommins
[IMSoP]
Thank you Rowan,
I wasn't following the discussion closely and didn't realize this was the issue. Thank you for taking the time to describe it.
For option 1:
Is manually copying the default also not type-safe? Is php a type-safe language? I think a lot of the arguments I saw suggested that people don't review libraries and their implementations when upgrading or installing them. This is just a shorthand for manually copy-pasting the default from other code, and this argument really only makes sense to me if there are no reviews before upgrading/using a library.
For option 3:
That being said, this is obviously playing with fire, and there will be people who (ab)use this and get burned; especially if they don't do due-diligence before using libraries. Thus a restriction may make a lot of sense; at least keeping it to the most obvious use cases should prevent the worst case scenarios imagined in this thread.
Realistically, I think we should only consider option (1) or (3). Option (3) -- if it can be done -- is the more conservative approach, and we can observe how it is used. We can always relax the feature in the future, based on feedback.
— Rob
Is manually copying the default also not type-safe? Is php a type-safe language? I think a lot of the arguments I saw suggested that people don't review libraries and their implementations when upgrading or installing them. This is just a shorthand for manually copy-pasting the default from other code, and this argument really only makes sense to me if there are no reviews before upgrading/using a library.
Copying and pasting the default value is no different from providing any other explicit value; whether you choose 69 because you like the number, or because you saw it was the current default, you are passing an integer. And if you write 23*3, you're just writing the same integer a different way.
PHP guarantees the type safety of this under inheritance by enforcing contravariance of input: if you try to write a subclass that would not accept an integer, when a parent class would, PHP will refuse to compile your subclass.
Similarly, if the class provides a non-final method like getDefaultFlags(): int
then it is type safe to call that and assume the value will always be an integer, because PHP enforces covariance of output: a subclass may not return a value that would not have been allowed by the parent class.
If you were designing a language where default values were explicitly available as outputs, you would need to make them covariant, which is option 2.
Obviously, the language cannot directly control the compatibility promises of third party libraries, but these principles are well enough known that I would expect popular projects to base their versioning / deprecation policies on them.
Regards,
Rowan Tommins
[IMSoP]