rfc:switch_expression

PHP RFC: Switch 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.

rfc/switch_expression.txt · Last modified: 2020/04/12 00:04 by ilijatovilo