rfc:default_expression

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
rfc:default_expression [2024/08/26 10:56] – Added note about unique object instances per default keyword bilgerfc:default_expression [2024/08/29 21:46] (current) – Added two seconary discussion concerns bilge
Line 1: Line 1:
 ====== PHP RFC: Default expression ====== ====== PHP RFC: Default expression ======
  
-  * Version: 1.0+  * Version: 1.1
   * Date: 2024-08-24   * Date: 2024-08-24
   * Author: Paul Morris <bilge@scriptfusion.com>   * Author: Paul Morris <bilge@scriptfusion.com>
Line 11: Line 11:
 ===== Introduction ===== ===== Introduction =====
  
-The only way to pass the default value to a function or method parameter is to not pass anything. This can be particularly difficult in some circumstances, because the language does not offer an intuitive way to pass //nothing//. This RFC proposes to make the existing keyword, <php>default</php>, the canonical way to pass the default value. Moreover, ''default'' becomes a valid expression in the function argument context, meaning it can be creatively combined with any and all existing PHP expression grammars to augment the default value as it is passed, including assigning it to variables (see [[#appendixdefault_expressions|appendix]] for a comprehensive list).+The only way to pass the default value to a function or method parameter is to not pass anything. This can be particularly difficult in some circumstances, because the language does not offer an intuitive way to pass //nothing//. This RFC proposes to make the existing keyword, <php>default</php>, the canonical way to pass the default value. Moreover, <php>default</php> becomes a valid expression in the function argument context, meaning it can be creatively combined with any and all existing PHP expression grammars to augment the default value as it is passed, including assigning it to variables (see [[#appendix_iidefault_expressions|appendix II]] for a comprehensive list).
  
 In its simplest form, <php>default</php> can be used as a single token. In its simplest form, <php>default</php> can be used as a single token.
Line 93: Line 93:
 Attempting to pass <php>default</php> to a function with no parameters, or as an argument beyond the callee's defined parameter limit, will result in a runtime exception. Similarly, attempting to pass <php>default</php> to a required parameter, with no default defined, results in a runtime exception. Attempting to pass <php>default</php> to a function with no parameters, or as an argument beyond the callee's defined parameter limit, will result in a runtime exception. Similarly, attempting to pass <php>default</php> to a required parameter, with no default defined, results in a runtime exception.
  
-Named arguments and default expressions can be used together, as in the following example.+==== Variadics ==== 
 + 
 +Variadic arguments do not permit a default expression. Internally, the variadic argument does not even count towards a function's formal argument count, so attempting to pass <php>default</php> to it results in the same runtime error you receive when passing an argument beyond the callee's limit. 
 + 
 +==== Named arguments ==== 
 + 
 +Named arguments and default expressions can be composed together, as in the following example.
  
 <code php> <code php>
 $f = fn ($v = 1, $default = 2) => $v + $default; $f = fn ($v = 1, $default = 2) => $v + $default;
-var_dump($f(default: default + 1)); // int(4)+$f(default: default + 1); // int(4)
 </code> </code>
  
-Internally, <php>default</php> is treated as a new opcode that causes the VM to perform a parameter default value lookup using reflection, albeit via an internal call that is more efficient than routing through the typical public interface. When <php>default</php> appears multiple times for the same argument, it is evaluated each time, causing each occurrence to point to a unique instance if the default is an object.+==== Match expression ==== 
 + 
 +Since [[match_expression_v2|match expressions]] also makes use of the <php>default</php> token, the grammar for the default match arm had to be migrated from the language parser to the compiler to prevent conflicts. The existing semantics were perfectly preserved, so the only difference one might notice is a slightly different error message when attempting something invalid like trying to share conditions with the default match arm. 
 + 
 +In the context of a function argument, match expressions may contain <php>default</php>, ergo the following is legal (albeit contrived). 
 + 
 +<code php> 
 +F(match (default) { 
 +    default => default 
 +}); 
 +</code> 
 + 
 +In the above example, the match expression and the match body (right of <php>=></php>) are the default //expression//, while the default in the match condition (left of <php>=></php>) is the special token denoting the default match arm to use when no other arm matches, the same as it was before this RFC. However, it is also possible to use the default expression as a condition, simply by combining it with any other expression syntax. 
 + 
 +Although there is no identity expression that works for all types, if you know the type of <php>default</php>, casting it to the same type is one way to convert it to an expression. 
 + 
 +<code php> 
 +$f = fn ($v = 1) => $v; 
 +$f(match (1) { 
 +    0 => 10, 
 +    (int) default => 20, 
 +    default => 30, 
 +}); // int(20) 
 +</code> 
 + 
 +Note that whilst multiple default arms are still prohibited, since we converted one of them to an expression, it is not acting as the default and does not count against this restriction. 
 + 
 +==== Internals ==== 
 + 
 +Internally, <php>default</php> is treated as a new opcode that causes the VM to perform a parameter default value lookup using reflection, albeit via an internal call that is more efficient than routing through the typical public interface. When <php>default</php> appears multiple times for the same argument, it is evaluated each time, causing each occurrence to point to a unique instance if the default is an object. If the lookup fails for any reason, a runtime exception will be thrown. 
 + 
 +Currently the only known failure case is lookup of trampoline functions, which can be created by calling <php>__invoke</php> on a closure, as in <php>(fn ($P = 1) => $P)->__invoke(default);</php>. Considering this is not the intended, nor even a documented way of invoking a closure, it is supposed this limitation is very minor. 
 + 
 +===== Discussion ===== 
 + 
 +The greatest concern is the proposed grammar is too permissive and has drawbacks. Secondary concerns include evaluating <php>default</php> in the calling context, and default values are now part of an object's public API. We will examine each of these issues in detail. 
 + 
 +==== Limiting grammar ==== 
 + 
 +The most common request is to constrain the allowed expression list. As already noted, some expressions don't make much sense because they probably don't have any practical application, and some are not comfortable allowing expressions that don't make sense into the language. This implies coming up with an [[https://externals.io/message/125183#125218|arbitrary exclusion list]] for certain expressions. Some proposed taking this a step further by [[https://externals.io/message/125183#125321|disallowing default as expression input]], which effectively rules out all operator classes except conditionals and invocations of <php>match()</php> that use <php>default</php> as a stand-alone output token. 
 + 
 +<code php> 
 +// Expressions with default as output only. 
 +F(1 ? default : 0) 
 +F(1 ? 1 : default) 
 +F(0 ?: default) 
 +F(null ?? default) 
 +F(match(1) { 1 => default }) 
 +</code> 
 + 
 +Further, some even expressed concerns about allowing any expressions at all and would only be comfortable allowing <php>default</php> as an isolated token, as in [[skipparams]]. 
 + 
 +Critics converged on a [[https://externals.io/message/125183#125274|valid counter-point]] that permitting expressions changing the default's type breaks LSP, as demonstrated in the following example (code courtesy of Ilija). Some have suggested this might be solved by disallowing <php>default</php> to be passed to union types (including <php>mixed</php>). 
 + 
 +<code php> 
 +class C { 
 +    public function F(int $V = 1) {} 
 +
 + 
 +class D extends C { 
 +    public function F(int|string $V = 's') {} 
 +
 + 
 +function test(C $C) { 
 +    $C->F(default + 1); 
 +
 + 
 +test(new C); // OK. 
 +test(new D); // Fatal error: Uncaught TypeError: Unsupported operand types: string + int. 
 +</code> 
 + 
 +==== Default as a dummy value ==== 
 + 
 +Currently <php>default</php>, as described by this RFC, is effectively replaced by the callee's default value and then passed to the callee from the caller, meaning the caller has full access to the default value. Some have [[https://externals.io/message/125183#125265|argued]] for an implementation that more literally follows the premise of this RFC, which is that <php>default</php> is just a dumb token that is standing in for //nothing//; it does not represent any value to the caller and merely instructs the callee to use its default value in the same way as when not passing the argument. 
 + 
 +==== Defaults as a contract ====
  
-If the lookup fails for any reason, a runtime exception will be thrown. Currently the only known cause of failure is lookup of trampoline functions, which can be created by calling <php>__invoke</php> on closure, as in <php>(fn ($P = 1) => $P)->__invoke(default);</php>. Considering this is not the intendednor even a documented way of invoking a closure, it is supposed this limitation is very minor.+Some have argued allowing <php>default</php> to read argument default values, previously only accessible via reflection, suddenly makes defaults part of an object's published API. However, changing a default is a behavioural change for any caller previously relying on those defaults (by not passing any argument)ergo defaults have always been part of the published API.
  
 ===== Backward Incompatible Changes ===== ===== Backward Incompatible Changes =====
Line 121: Line 202:
 </doodle> </doodle>
  
-===== Appendix: Default expressions =====+Regardless of how you vote above, we'd like to collect feedback on which limitations of this proposal would make it/still be acceptable for you, starting from the least significant to the most significant changes. For details on what each of these options mean, see [[#discussion|discussion]]. You can vote for multiple options, but if you choose the last one then it doesn't make much sense to pick any others.
  
-Throughout this RFC we have referred to default as an //expression//, but the only concrete examples of expressions have been the conditional expressions using the null coalesce (<php>??</php>) and ternary (<php>?:</php>) operators. This appendix lists all the ways <php>default</php> can be combined with other tokens and operators to form valid expressions, but before getting to that list, let's look at one more non-conditional example using the binary pipe operator (<php>|</php>) to augment flags.+<doodle title="Which limited grammars would you support?" auth="bilge" voteType="multi" closed="false" closeon="2024-08-09T21:00:00Z"> 
 +   * No union types 
 +   * Only conditional expressions 
 +   * No expressions 
 +   * No default in arguments 
 +</doodle> 
 + 
 +Only the result of the primary vote has a clear path forward for inclusion into the language. None of the proposed alternatives come with any feasibility guarantees. Howeverif the primary vote fails, the secondary vote may inform whomsoever wishes to pursue a follow-up for limited application of ''default'', which requires a new RFC and implementation for that counter-proposal. 
 + 
 +===== Appendix I: Further examples ===== 
 + 
 +The main examples of default expressions so far have been conditional expressions using the null coalesce (<php>??</php>) and ternary (<php>?:</php>) operators. Let's look at one more non-conditional example using the binary pipe operator (<php>|</php>) to augment flags.
  
 <code php> <code php>
Line 136: Line 228:
 This static class wraps the internal function, <php>json_encode</php> to provide saner defaults for JSON encoding. In particular, it sets the <php>JSON_THROW_ON_ERROR</php> flag so our return type is guaranteed to be string, eliminating the possibility we have to deal with <php>false</php> as a return type. For the purposes of this contrived example, it is required to ignore the fact that the flag should be specified in the function body rather than the parameter default, to ensure the caller doesn't break this contract. Supposing we want to pretty-print our encoded JSON, we could call: <code php>Json::encode([], JSON_PRETTY_PRINT);</code> However, this will override the throw on error flag, which we want to keep. Calling the function as: <code php>Json::encode([], default | JSON_PRETTY_PRINT);</code> allows us to keep the default and append our pretty-print flag, which continues to work even if the ''Json'' class should elect to update its defaults later. This static class wraps the internal function, <php>json_encode</php> to provide saner defaults for JSON encoding. In particular, it sets the <php>JSON_THROW_ON_ERROR</php> flag so our return type is guaranteed to be string, eliminating the possibility we have to deal with <php>false</php> as a return type. For the purposes of this contrived example, it is required to ignore the fact that the flag should be specified in the function body rather than the parameter default, to ensure the caller doesn't break this contract. Supposing we want to pretty-print our encoded JSON, we could call: <code php>Json::encode([], JSON_PRETTY_PRINT);</code> However, this will override the throw on error flag, which we want to keep. Calling the function as: <code php>Json::encode([], default | JSON_PRETTY_PRINT);</code> allows us to keep the default and append our pretty-print flag, which continues to work even if the ''Json'' class should elect to update its defaults later.
  
-Following is the full expression list involving <php>default</php>. Not all of these examples will make much (or any) sense in practical terms, particularly those towards the end of the list, but many may have uses depending on your requirements and creativity.+===== Appendix II: Default expressions ===== 
 + 
 +Throughout this RFC we have referred to default as an //expression//. This appendix lists all the ways <php>default</php> as an expression can be composed with other expressions. Not all of these examples will make much (or any) sense in practical terms, particularly those towards the end of the list, yet many may have uses depending on your requirements and creativity.
  
 <code php> <code php>
Line 211: Line 305:
 // Match // Match
 F(match(default) { default => default }) F(match(default) { default => default })
 +
 +// Callable
 +F((default)->M())
  
 // Parens // Parens
rfc/default_expression.1724669809.txt.gz · Last modified: 2024/08/26 10:56 by bilge