This is an old revision of the document!
PHP RFC: Switch expression
- Date: 2020-03-28
- Author: Ilija Tovilo, ilija.tovilo@me.com
- Status: Draft
- Target Version: PHP 8.0
- Implementation: https://github.com/php/php-src/pull/5308
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 = $condition switch { 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 = $x switch { 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 ,
.
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 exception. 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 InvalidArgumentException
if the condition isn't met for any of the cases and the switch
doesn't contain a default
case.
$x switch { 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
The syntax is closely designed after C#'s switch expression.
Some people have asked why we don't reuse the syntax of the switch
statement.
$x = switch ($y) {} // instead of $x = $y switch {}
While this would be the preferred choice it is ambiguous.
switch ($x) {} [$a] = ...; // Could also be interpreted as switch ($y) {}[$a] = ...;
Allowing this syntax would require making the parser context-sensitive which is undesirable. Another option would be to use a different keyword (e.g. match
) which would be a large backward incompatible change.
"Why don't you just use x"
The 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 $x switch { 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.