Re: [PHP-DEV] [RFC] switch expression

This is only part of a thread. view whole thread
  109291
March 25, 2020 14:17 michal.brzuchalski@gmail.com (=?UTF-8?Q?Micha=C5=82_Brzuchalski?=)
Hi Ilija,

śr., 25 mar 2020 o 13:10 Ilija Tovilo tovilo@me.com> napisał(a):

> Hi everybody! > > > > A few years ago I suggested adding a new `match` expression to the PHP > language: > > https://externals.io/message/100487 > > > > I arrogantly assumed someone will implement it for me which of course > didn't happen. I'd finally like to get my own hands dirty. I have a very > rough, incomplete prototype but I'd like to get your feedback before I > continue working on the details. > > > > # Introduction > > > > This is what it looks like: > > > > ```php > > echo $i switch { > > 0 => "i equals 0", > > 1 => "i equals 1", > > 2 => "i equals 2", > > 3, 4 => "i equals 3 or 4", > > }; > > > > // is roughly equivalent to > > > > switch ($i) { > > case 0: > > $tmp = "i equals 0"; > > break; > > case 1: > > $tmp = "i equals 1"; > > break; > > case 2: > > $tmp = "i equals 2"; > > break; > > case 3: > > case 4: > > $tmp = "i equals 3 or 4"; > > break; > > default: > > throw new InvalidArgumentException('Unhandled switch case'); > > } > > > > echo $tmp; > > ``` > > > > Some things to note: > > > > * Each case only accepts a single expression > > * The entire switch expression evaluates to the result of the executed case > > * There is no fallthrough, an implicit break is added after every case > > * Multiple case conditions are possible with comma separation > > * The default case throws a InvalidArgumentException by default > > * The switch keyword is used as an infix operator > > > > # Syntax > > > > Originally, I expected to reuse the current syntax and transform it into > an expression. > > > > ```php > > $x = switch ($y) { ... }; > > ``` > > > > Turns out this is ambiguous. > > > > ```php > > switch ($y) { ... } > > [$a] = ...; > > > > // Could also be interpreted as > > switch ($y) { ... }[$a] = ...; > > ``` > > > > I stole the new syntax from C# 8.0 which means at least some people will > already be familiar with it: > > > https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/may/csharp-8-0-pattern-matching-in-csharp-8-0#the-evolution-of-pattern-matching-in-c-80 > > > > # Type coercion > > > > One of the bigger weak points of the `switch` statement is the fact that > it performs implicit type coercion. > > > > ```php > > switch ('foo') { > > case 0: > > echo "Oh no!\n"; > > } > > ``` > > > > While it's very tempting to fix this in the new `switch` expression it > adds a confusing discrepancy between the `switch` statement and expression. > I think it would be preferrable to keep the two the same and change the > behavior of both in a new PHP edition ( > https://github.com/php/php-rfcs/pull/2). > > > > # Pattern matching > > > > I decided against pattern matching because PHP doesn't have algebraic data > types and classes rarely have public properties. In my opinion the limited > use cases don't justify the significant complexity added to the language. > It would also, once again, add an unjustified discrepancy between the > `switch` statement and expression. If at some point we do want to introduce > pattern matching it might be better to introduce a different keyword (e.g.. > `match`) and make it work for both the statement and expression. In case > you need to match a more complex expression the following still works fine: > > > > ```php > > echo true switch { > > is_int($x) => 'int', > > is_float($x) => 'float', > > is_string($x) => 'string', > > ... > > }; > > ``` > > > > # Blocks > > > > Sometimes it would be useful to split the expression into multiple > statements to make it more readable. Unfortunately, in PHP there are no > block expressions. Rust allows returning the last value by omitting the > semicolon: > > > > ```php > > echo $x switch { > > 1 => { > > foo(); > > bar(); > > baz() > > }, > > }; > > ``` > > > > This is indeed possible in PHP and could be implemented as part of the > `switch` expression or as a general language feature. A nice side effect is > that this could also be used in arrow functions: > > > > ```php > > $x = fn() => { > > foo(); > > bar(); > > baz() > > }; > > ``` > > > > This would, however, make it inconsistent with closures as they use the > return keyword. Thus we would probably have to make sure arrow functions > still work with return statement which would decrease the need for such a > language construct. It is also very unlike anything else in PHP. > > > > # Poll > > > > This is a short overview of what I'll be working on in the coming weeks. I > created a short poll for you guys to let me know if this idea is worth > pursuing: > > https://forms.gle/stXMv72CAaDDxfwf8 > > > > Stay safe! > > That looks like what I've described a few months ago in
https://wiki.php.net/rfc/switch-expression-and-statement-improvement If you dig into the mailing list you can even find almost ready to use patch which implements it. I'd love switch expression inclusion in PHP. Cheers, Michał Brzuchalski
  109293
March 25, 2020 14:27 ilija.tovilo@me.com (Ilija Tovilo)
Hi Michał

 

I’m sorry, unfortunately I missed your e-mail and RFC.

Let me know if you’re still working on it and I’ll back off of course.

 

Regards
  109294
March 25, 2020 14:59 larry@garfieldtech.com ("Larry Garfield")
On Wed, Mar 25, 2020, at 9:27 AM, Ilija Tovilo wrote:
> Hi Michał > > > > I’m sorry, unfortunately I missed your e-mail and RFC. > > Let me know if you’re still working on it and I’ll back off of course. > > > > Regards
I like the concept, and it looks like you're both on a similar track. Give or take details, I would very much like to see something like it. One possible improvement to either version is allowing an expression on the left side. That is, rather than doing an equality match, do a boolean match. That would then allow: $foo = switch($bar) { case $bar < 5 => $bar * 3; case $baz < 10 => $bar * 4; default => $bar * 5; }; That would sidestep the need for pattern matching, as you can do anything an expression can do. The obvious caveat of course is figuring out how to reference the variable being switched on, if it's not already a variable. My first thought there is to borrow the $$ variable name from Sara's old function composition proposal, but there may be others. There's likely other issues to discuss here but allowing expressions on the left would greatly improve the expressiveness of the construct. I'd prefer to not allow multi-line statements on the right, ie, blocks. That leads to too much potential for long and ugly code, which a construct like this should be avoiding. Limiting it to a single expression keeps it compact; if you have more involved logic, then put it in a function and your expression is just... calling that function. Problem solved.. Side note: I did a limited user-space implementation of the same concept a while back: https://hive.blog/php/@crell/type-matching-in-php But I'd definitely rather see it in the native syntax. --Larry Garfield
  109295
March 25, 2020 15:29 ilija.tovilo@me.com (Ilija Tovilo)
Thanks for your feedback, Larry!

> One possible improvement to either version is allowing an expression on the left side. That is, rather than doing an equality match, do a boolean match.
This is how Rust does it: ```rust let x = match ... { Some(y) if y < 5 => ... } ``` In other words, you can add an additional guard to each case that excepts any expression. We don't really benefit a lot from that since we don't have pattern matching. I don't think this would add any significant benefit over: ```php $x = true switch { $x !== null && $x < 5 => ... } ``` Regards
  109296
March 25, 2020 15:46 larry@garfieldtech.com ("Larry Garfield")
On Wed, Mar 25, 2020, at 10:29 AM, Ilija Tovilo wrote:
> Thanks for your feedback, Larry! > > > One possible improvement to either version is allowing an expression on the left side. That is, rather than doing an equality match, do a boolean match. > > This is how Rust does it: > > ```rust > let x = match ... { > Some(y) if y < 5 => ... > } > ``` > > In other words, you can add an additional guard to each case that > excepts any expression. We don't really benefit a lot from that since > we don't have pattern matching. I don't think this would add any > significant benefit over: > > ```php > $x = true switch { > $x !== null && $x < 5 => ... > } > ```
Good point, I'd forgotten about that potential trick. So as long as an expression is allowed on the left, rather than just a literal, which is then == compared against the provided value, that should be "good enough" for most use cases. The implementation should include some tests to make sure that works properly, but I'm happy with the resulting syntax. So then the net result is: $var = switch($val) { case expr1 => expr2; } Where $val gets compared against the result of each expr1, and if true then $var is set to expr2. Endorse. --Larry Garfield
  109297
March 25, 2020 16:06 php@dennis.birkholz.biz (Dennis Birkholz)
Hello together,

Am 25.03.20 um 16:46 schrieb Larry Garfield:
> On Wed, Mar 25, 2020, at 10:29 AM, Ilija Tovilo wrote: >> Thanks for your feedback, Larry! >> >>> One possible improvement to either version is allowing an expression on the left side. That is, rather than doing an equality match, do a boolean match. >> >> This is how Rust does it: >> >> ```rust >> let x = match ... { >> Some(y) if y < 5 => ... >> } >> ``` >> >> In other words, you can add an additional guard to each case that >> excepts any expression. We don't really benefit a lot from that since >> we don't have pattern matching. I don't think this would add any >> significant benefit over: >> >> ```php >> $x = true switch { >> $x !== null && $x < 5 => ... >> } >> ``` > > Good point, I'd forgotten about that potential trick. So as long as an expression is allowed on the left, rather than just a literal, which is then == compared against the provided value, that should be "good enough" for most use cases. > > The implementation should include some tests to make sure that works properly, but I'm happy with the resulting syntax. > > So then the net result is: > > $var = switch($val) { > case expr1 => expr2; > } > > Where $val gets compared against the result of each expr1, and if true then $var is set to expr2.
on the first glance this all looks nice but you actually created something more like an if-expression that uses switch as a keyword because you stripped switch of some of its major features: - you compare the given value to possible cases -> you compare expressions to true - you can fall through to other cases without break - what about the default case? What about the following if-expression-syntax: $var = if ($x > 0) { return 1; } elseif ($x < 0) { return -1; } else { return 0; } Maybe this is more in line of what you want to do with your switch expression? Greets Dennis
  109300
March 25, 2020 16:22 ilija.tovilo@me.com (Ilija Tovilo)
Hi Dennis

Thanks for your feedback!

> you can fall through to other cases without break
You could do the same using the || operator.
> what about the default case?
I haven't described the default case in my proposal but it is exactly what you'd expect it to be: ```php $var = true switch { $x > 0 => 1, $x < 0 => -1, default => 0, }; ```
> What about the following if-expression-syntax:
That would work (once again, Rust already does it) though not with the return keyword. We'd still need a block expression to pass the value from the block to the if expression. When I compare the two I definitely think the match expression is more readable. Regards
  109303
March 25, 2020 16:51 cmbecker69@gmx.de ("Christoph M. Becker")
On 25.03.2020 at 17:06, Dennis Birkholz wrote:
> Hello together, > > Am 25.03.20 um 16:46 schrieb Larry Garfield: >> On Wed, Mar 25, 2020, at 10:29 AM, Ilija Tovilo wrote: >>> Thanks for your feedback, Larry! >>> >>>> One possible improvement to either version is allowing an expression on the left side. That is, rather than doing an equality match, do a boolean match. >>> >>> This is how Rust does it: >>> >>> ```rust >>> let x = match ... { >>> Some(y) if y < 5 => ... >>> } >>> ``` >>> >>> In other words, you can add an additional guard to each case that >>> excepts any expression. We don't really benefit a lot from that since >>> we don't have pattern matching. I don't think this would add any >>> significant benefit over: >>> >>> ```php >>> $x = true switch { >>> $x !== null && $x < 5 => ... >>> } >>> ``` >> >> Good point, I'd forgotten about that potential trick. So as long as an expression is allowed on the left, rather than just a literal, which is then == compared against the provided value, that should be "good enough" for most use cases. >> >> The implementation should include some tests to make sure that works properly, but I'm happy with the resulting syntax. >> >> So then the net result is: >> >> $var = switch($val) { >> case expr1 => expr2; >> } >> >> Where $val gets compared against the result of each expr1, and if true then $var is set to expr2. > > on the first glance this all looks nice but you actually created > something more like an if-expression that uses switch as a keyword > because you stripped switch of some of its major features: > - you compare the given value to possible cases -> you compare > expressions to true > - you can fall through to other cases without break > - what about the default case? > > What about the following if-expression-syntax: > > $var = if ($x > 0) { return 1; } > elseif ($x < 0) { return -1; } > else { return 0; } > > Maybe this is more in line of what you want to do with your switch > expression?
Or maybe even $var = $x > 0 ? 1 :($x < 0 ? -1 : 0); Yes, the required parentheses are ugly, but in my opinion, this is still better than a new if or switch(true) expression construct. -- Christoph M. Becker
  109299
March 25, 2020 16:21 rowan.collins@gmail.com (Rowan Tommins)
On Wed, 25 Mar 2020 at 15:29, Ilija Tovilo tovilo@me.com> wrote:

> I don't think this would add any significant benefit over: > > ```php > $x = true switch { > $x !== null && $x < 5 => ... > } > ``` >
The problem with that is that it requires a temporary variable to be switched on. If I want to switch on, say, a method call, I can write this for equality: $result = $this->foo($bar) switch { 1 => 'hello', 2 => 'hi', 3 => 'goodbye' }; For inequalities, the switch(true) version looks something like this (the parentheses would probably be optional, but I'd personally use them for readability): $temp = $this->foo($bar); $result = true switch { ($temp <= 1) => 'hello', ($temp == 2) => 'hi', default => 'goodbye' } Using $$ to mean "value tested" would mean you could get rid of the temp variable, and just write this: $result = $this->foo($bar) switch { ($$ <= 1) => 'hello', ($$ == 2) => 'hi', default => 'goodbye' }; I've also previously thought about specifying an operator for switch statements, which could be used with expressions as well, e.g. $result = $this->foo($bar) switch <= { 1 => 'hello', 2 => 'hi', default => 'goodbye' }; Again, though, this is something that could be added to switch statements and expressions together, as a separate RFC. Regards, -- Rowan Tommins [IMSoP]
  109301
March 25, 2020 16:28 ilija.tovilo@me.com (Ilija Tovilo)
Hi Rowan

> The problem with that is that it requires a temporary variable to be > switched on. If I want to switch on, say, a method call, I can write this > for equality:
I agree. The iffy part would be recognizing if the case expression should be equated to the switch input or evaluated on its own. That is still something we could address in a later RFC for both the statement and expression. Regards
  109306
March 25, 2020 18:24 php@manuelcanga.dev (Manuel Canga)
Hi, internals,


 ---- En mié, 25 mar 2020 17:21:29 +0100 Rowan Tommins collins@gmail.com> escribió ----
 > On Wed, 25 Mar 2020 at 15:29, Ilija Tovilo tovilo@me.com> wrote:
 > 
 > > I don't think this would add any significant benefit over:
 > >
 > > ```php
 > > $x = true switch {
 > >     $x  !== null && $x < 5 => ...
 > > }
 > > ```
 > >
 > 
 > 
 > The problem with that is that it requires a temporary variable to be
 > switched on. If I want to switch on, say, a method call, I can write this
 > for equality:
 > 
 > $result = $this->foo($bar) switch {
 >     1 => 'hello',
 >     2 => 'hi',
 >     3 => 'goodbye'
 > };

In this case, you can also do:

$result =  [
    1 => 'hello',
    2 => 'hi',
    3 => 'goodbye'
 ][$this->foo($bar)];

With syntax very similar to proposed switch but this is using array.

Regards
 --
Manuel Canga