====== PHP RFC: Switch expression ====== * Date: 2020-03-28 * Author: Ilija Tovilo, tovilo.ilija@gmail.com * Author: MichaƂ Brzuchalski, brzuchal@php.net * Status: Withdrawn * Target Version: PHP 8.0 * Implementation: https://github.com/php/php-src/pull/5308 * Previous RFC: https://wiki.php.net/rfc/switch-expression-and-statement-improvement * Superseded by RFC: https://wiki.php.net/rfc/match_expression ===== Introduction ===== The ''switch'' statement has some long-standing issues that we're going to look at in this RFC. * Returning values * Fallthrough * Inexhaustiveness * Type coercion ===== Proposal ===== This RFC proposes to introduce an expression variant of the ''switch'' statement that addresses some of the issues mentioned above. $expressionResult = switch ($condition) { 1 => foo(), 2 => bar(), 3, 4, 5 => baz(), }; It also suggests allowing multiple case conditions implemented for the ''switch'' expression in the statement. switch ($condition) { case 1: foo(); break; case 2: bar(); break; case 3, 4, 5: baz(); break; } ===== Issues ===== We're going to take a look at each issue and how we can improve the ''switch'' statement or expression in that regard. ==== Returning values ==== It is very common that the ''switch'' produces some value that is used after the ''switch'' statement. switch ($x) { case 0: $y = 'Foo'; break; case 1: $y = 'Bar'; break; case 2: $y = 'Baz'; break; } var_dump($y); It is easy to forget assigning ''$y'' in one of the cases. It is also visually unintuitive to find ''$y'' declared in a deeper nested scope. This is the main motivation for introducing a ''switch'' expression that allows returning values from cases in a more natural way. $y = switch ($x) { 0 => 'Foo', 1 => 'Bar', 2 => 'Baz', }; var_dump($y); ==== Fallthrough ==== The ''switch'' fallthrough has been a large source of bugs in many languages. Each ''case'' must explicitely ''break'' out of the ''switch'' statement or the execution will continue into the ''case'' even if the condition is not met. switch ($pressedKey) { case Key::ENTER: save(); // Oops, forgot the break case Key::DELETE: delete(); break; } This was intended to be a feature so that multiple conditions can execute the same block of code. switch ($x) { case 1: case 2: // Same for 1 and 2 break; case 3: // Only 3 case 4: // Same for 3 and 4 } It is often hard to understand if the missing ''break'' was the authors intention or a mistake. Many modern languages avoid this issue by implicitly breaking out of the ''case''. Multiple conditions can be provided to the same ''case'' so that they execute the same block. There's often no way to achieve the same result as 3 and 4 in the example above without an additional ''if'' statement. switch ($x) { case 1, 2: // Same for 1 and 2 case 3, 4: if ($x === 3) { // Only 3 } // Same for 3 and 4 } The fallthrough behavior can't reasonably be changed in the ''switch'' statement because it would break a lot of code. However this RFC porposes allowing multiple conditions per ''case'' so that the intention of running the same code can be expressed more clearly. The ''switch'' expression resolves this issue exactly as described above. There is an implicit ''break'' added after each ''case''. Like with the statement multiple ''case'' conditions can be separated by a comma. ==== Inexhaustiveness ==== Another large source of bugs is not handling all the possible cases supplied to the ''switch'' statement. switch ($x) { case 1: // ... break; case 2: // ... break; } // $x is a 3? I never expected a 3 The unexpected value will go unnoticed until the program crashes in a weird way, causes strange behavior or even worse becomes a security hole. Many languages can check if all the cases are handled at compile time or force you to write a ''default'' case if they can't. For a dynamic language like PHP the only alternative is throwing an error. We can't reasonably change the exhaustiveness behavior in the ''switch'' statement because it would break a lot of code. The ''switch'' expression resolves this issue by throwing an ''UnhandledSwitchCaseError'' if the condition isn't met for any of the cases and the ''switch'' doesn't contain a ''default'' case. switch ($x) { 1 => ..., 2 => ..., }; // $x can never be 3 ==== Type coercion ==== The ''switch'' statement loosely compares the given value to the case values. This can lead to some very unexpected results. switch ('foo') { case 0: echo "Oh no!\n"; break; } It is very tempting to fix this issue for the ''switch'' expression. This RFC proposes not to do so as it would add an arbitrary distinction between the ''switch'' statement and expression. Hopefully this is something that can be addressed in the future for both the statement and expression (see chapter "Fixing the statement"). ===== Fixing the statement ===== There is a proposal to introduce [[https://github.com/php/php-rfcs/pull/2|editions to PHP]] that allow for bigger backward incompatible changes. This would be a perfect opportunity to fix the undesirable behavior in the ''switch'' statement. This is, however, not a part of this RFC. ===== Expression syntax ===== There is an ambiguity problem with the empty ''switch'' statement vs expression: // Could be a switch expression or a switch statement with an empty statement (;) switch ($x) {}; To resolve it ambiguity empty switch expressions are not disallowed. // This code throws a parser error $x = switch ($y) {}; ===== "Why don't you just use x" ===== There have been some comments on how you can already achieve the same result. ==== if statements ==== if ($x === 1) { $y = ...; } elseif ($x === 2) { $y = ...; } elseif ($x === 3) { $y = ...; } Needless to say this is incredibly verbose and there's a lot of repetition. It also can't make use of the switches jumptable optimization. You must also not forget to write an ''else'' statement to catch unwanted values. ==== Hash maps ==== $y = [ 1 => ..., 2 => ..., ][$x]; This code will execute every single "case", not just the one that is finally chosen. It will also build a hash map in memory every time the ''switch'' is executed. And again, you must not forget to handle unwanted values. ==== Nested ternary operators ==== $y = $x === 1 ? ... : ($x === 2 ? ... : ($x === 3 ? ... : 0)); The parentheses make it hard to read and it's easy to make mistakes and there is no jumptable optimization. Adding more cases will make the situation worse. ===== Future scope ===== As mentioned each ''case'' in the ''switch'' expression can only contain a single expression. We could allow passing blocks to the ''case'' in the future but this is not part of this RFC. echo switch ($x) { 1 => { foo(); bar(); baz() // Rust style return value by omitting semicolon }, }; ===== Backward Incompatible Changes ===== There are no breaking changes in this RFC. ===== Proposed PHP Version(s) ===== The proposed version is PHP 8. ===== Proposed Voting Choices ===== As this is a language change, a 2/3 majority is required. The vote is a straight Yes/No vote for accepting the RFC and merging the patch.