====== PHP RFC: Match expression v2 ======
* Date: 2020-05-22
* Author: Ilija Tovilo, tovilo.ilija@gmail.com
* Status: Implemented
* Target Version: PHP 8.0
* Implementation: https://github.com/php/php-src/pull/5371
* Supersedes: https://wiki.php.net/rfc/match_expression
===== Proposal =====
This RFC proposes adding a new ''%%match%%'' expression that is similar to ''%%switch%%'' but with safer semantics and the ability to return values.
[[https://github.com/doctrine/orm/blob/72bc09926df1ff71697f4cc2e478cf52f0aa30d8/lib/Doctrine/ORM/Query/Parser.php#L816|From the Doctrine query parser]]:
// Before
switch ($this->lexer->lookahead['type']) {
case Lexer::T_SELECT:
$statement = $this->SelectStatement();
break;
case Lexer::T_UPDATE:
$statement = $this->UpdateStatement();
break;
case Lexer::T_DELETE:
$statement = $this->DeleteStatement();
break;
default:
$this->syntaxError('SELECT, UPDATE or DELETE');
break;
}
// After
$statement = match ($this->lexer->lookahead['type']) {
Lexer::T_SELECT => $this->SelectStatement(),
Lexer::T_UPDATE => $this->UpdateStatement(),
Lexer::T_DELETE => $this->DeleteStatement(),
default => $this->syntaxError('SELECT, UPDATE or DELETE'),
};
===== Differences to switch =====
==== Return value ====
It is very common that the ''%%switch%%'' produces some value that is used afterwards.
switch (1) {
case 0:
$result = 'Foo';
break;
case 1:
$result = 'Bar';
break;
case 2:
$result = 'Baz';
break;
}
echo $result;
//> Bar
It is easy to forget assigning ''%%$result%%'' in one of the cases. It is also visually unintuitive to find ''%%$result%%'' declared in a deeper nested scope. ''%%match%%'' is an expression that evaluates to the result of the executed arm. This removes a lot of boilerplate and makes it impossible to forget assigning a value in an arm.
echo match (1) {
0 => 'Foo',
1 => 'Bar',
2 => 'Baz',
};
//> Bar
==== No type coercion ====
The ''%%switch%%'' statement loosely compares (''%%==%%'') the given value to the case values. This can lead to some very surprising results.
switch ('foo') {
case 0:
$result = "Oh no!\n";
break;
case 'foo':
$result = "This is what I expected\n";
break;
}
echo $result;
//> Oh no!
The ''%%match%%'' expression uses strict comparison (''%%===%%'') instead. The comparison is strict regardless of ''%%strict_types%%''.
echo match ('foo') {
0 => "Oh no!\n",
'foo' => "This is what I expected\n",
};
//> This is what I expected
==== No fallthrough ====
The ''%%switch%%'' fallthrough has been a large source of bugs in many languages. Each ''%%case%%'' must explicitly ''%%break%%'' out of the ''%%switch%%'' statement or the execution will continue into the next ''%%case%%'' even if the condition is not met.
switch ($pressedKey) {
case Key::RETURN_:
save();
// Oops, forgot the break
case Key::DELETE:
delete();
break;
}
The ''%%match%%'' expression resolves this problem by adding an implicit ''%%break%%'' after every arm.
match ($pressedKey) {
Key::RETURN_ => save(),
Key::DELETE => delete(),
};
Multiple conditions can be comma-separated to execute the same block of code.
echo match ($x) {
1, 2 => 'Same for 1 and 2',
3, 4 => 'Same for 3 and 4',
};
==== Exhaustiveness ====
Another large source of bugs is not handling all the possible cases supplied to the ''%%switch%%'' statement.
switch ($operator) {
case BinaryOperator::ADD:
$result = $lhs + $rhs;
break;
}
// Forgot to handle BinaryOperator::SUBTRACT
This will go unnoticed until the program crashes in a weird way, causes strange behavior or even worse becomes a security hole. ''%%match%%'' throws an ''%%UnhandledMatchError%%'' if the condition isn’t met for any of the arms. This allows mistakes to be caught early on.
$result = match ($operator) {
BinaryOperator::ADD => $lhs + $rhs,
};
// Throws when $operator is BinaryOperator::SUBTRACT
===== Miscellaneous =====
==== Arbitrary expressions ====
A match condition can be any arbitrary expression. Analogous to ''%%switch%%'' each condition will be checked from top to bottom until the first one matches. If a condition matches the remaining conditions won’t be evaluated.
$result = match ($x) {
foo() => ...,
$this->bar() => ..., // bar() isn't called if foo() matched with $x
$this->baz => ...,
// etc.
};
===== Future scope =====
==== Blocks ====
In this RFC the body of a match arm must be an expression. Blocks for match and arrow functions will be discussed in a separate RFC.
==== Pattern matching ====
[[https://github.com/php/php-src/compare/master...iluuu1994:pattern-matching|I have experimented with pattern matching]] and decided not to include it in this RFC. Pattern matching is a complex topic and requires a lot of thought. Each pattern should be discussed in detail in a separate RFC.
==== Allow dropping (true) ====
$result = match { ... };
// Equivalent to
$result = match (true) { ... };
===== Backward Incompatible Changes =====
''%%match%%'' was added as a keyword (''%%reserved_non_modifiers%%''). This means it can’t be used in the following contexts anymore:
* namespaces
* class names
* function names
* global constants
Note that it will continue to work in method names and class constants.
===== Syntax comparison =====
https://gist.github.com/iluuu1994/11ac292cf7daca8162798d08db219cd5
===== Vote =====
Voting starts 2020-06-19 and ends 2020-07-03.
As this is a language change, a 2/3 majority is required.
* Yes
* No