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