【PHP8.0】厳密なswitch文ことmatch式が使えるようになる

PHPがよく言われる問題点のひとつとして、[switch](https://www.php.net/manual/ja/control-structures.switch.php)が[曖昧な比較である](https://qiita.com/GHKEN/items/e2c96324a0365a690e86)ということが挙げられます。

```php
switch($x){
	case 1:
		'$xは1だよ';
		break;
	case "1":
		'$xは"1"だよ';
		break;
}
```

`case "1"`に到達することは決してありません。

ということで厳密な比較を用いるswitchことmatch構文のRFCが提出されました。
以下は[Match expression v2](https://wiki.php.net/rfc/match_expression_v2)の日本語訳です。


# PHP RFC: Match expression v2

## Proposal

このRFCは、switchに似ていますが、より安全なセマンティクスを持つmatch構文の提案です。

例として、[Doctrineのクエリパーサ](https://github.com/doctrine/orm/blob/72bc09926df1ff71697f4cc2e478cf52f0aa30d8/lib/Doctrine/ORM/Query/Parser.php#L816)を挙げます。

```php
// 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

switch構文と異なる点です。

### Return value

後で使いたい値をswitch内で生成することは非常によくあることです。

```php
switch (1) {
    case 0:
        $result = 'Foo';
        break;
    case 1:
        $result = 'Bar';
        break;
    case 2:
        $result = 'Baz';
        break;
}

echo $result; //> Bar
```

そして`$result`に代入し忘れることもよくあるミスです。
さらに深くネストされていた場合は、`$result`がしっかり代入されているか確認するのもたいへんです。
それに対し、`match`は実行した結果が評価される式です。
これによって多くの定型文を削除することができ、代入忘れというミスがなくなります。

```php
echo match (1) {
    0 => 'Foo',
    1 => 'Bar',
    2 => 'Baz',
};
//> Bar
```

### No type coercion

switch文は緩やかな比較`==`を使います。
これは直感に反する結果をもたらすことがあります。

```php
switch ('foo') {
    case 0:
      $result = "Oh no!\n";
      break;
    case 'foo':
      $result = "This is what I expected\n";
      break;
}

echo $result;
//> Oh no!
```

match式は厳密な比較`===`で比較します。
`strict_types`の設定に関わらず常に厳密です。

```php
echo match ('foo') {
    0 => "Oh no!\n",
    'foo' => "This is what I expected\n",
};
//> This is what I expected
```


### No fallthrough

switchフォールスルーは、多くの言語でバグの温床となっています。
各`case`は明示的に`break`しないかぎり、次の`case`へと実行が継続されます。

```php
switch ($pressedKey) {
    case Key::RETURN_:
        save();
        // break忘れた
    case Key::DELETE:
        delete();
        break;
}
```

match式では、暗黙の`break`を付与することで、この問題を解決します。

```php
match ($pressedKey) {
    Key::RETURN_ => save(),
    Key::DELETE => delete(),
};
```

複数条件で同じコードを実行したい場合は、条件をカンマで区切ります。

```php
echo match ($x) {
    1, 2 => 'Same for 1 and 2',
    3, 4 => 'Same for 3 and 4',
};
```

### Exhaustiveness

switchでよくあるもうひとつの問題は、全てのcaseに対応していない場合の処理です。

```php
switch ($operator) {
    case BinaryOperator::ADD:
        $result = $lhs + $rhs;
        break;
}

// BinaryOperator::SUBTRACTを渡しても何も起こらない
```

これが原因で、よくわからないところでクラッシュしたり、想定していない動作をしたり、なお悪いときにはセキュリティホールの原因になったりします。

```php
$result = match ($operator) {
    BinaryOperator::ADD => $lhs + $rhs,
};

// BinaryOperator::SUBTRACTを渡すと例外が発生する
```

match式はどのcaseにも当てはまらなかった場合はUnhandledMatchErrorを発するので、間違いに早期に気付くことができます。

## Miscellaneous

### Arbitrary expressions

matchする条件を任意の式にすることができます。
比較条件はswitch同様上から順に判定され、マッチした以後の条件は評価されません。

```php
$result = match ($x) {
    foo() => ...,
    $this->bar() => ..., // foo()がマッチしたらここは呼ばれない
    $this->baz => ...,
    // etc.
};
```

## Future scope

この項目は将来の予定であり、このRFCには含まれません。

### Blocks

このRFCでは、match式の本文はひとつの式でなければなりません。
ブロックを許すかについては、別のRFCで議論します。

### Pattern matching

[パターンマッチングについても検討しました](https://github.com/php/php-src/compare/master...iluuu1994:pattern-matching)が、このRFCには含めないことにしました。
パターンマッチングは非常に複雑であり、多くの調査が必要です。
パターンマッチングについては別のRFCで議論します。

### Allow dropping (true)

```php
$result = match { ... };
// ↓と同じ
$result = match (true) { ... };
```

## Backward Incompatible Changes

`match`がキーワード`reserved_non_modifiers`として追加されます。
以下のコンテキストで使用することができなくなります。
・名前空間
・クラス名
・関数名
・グローバル定数

メソッド名およびクラス定数としては引き続き使用可能です。

## Syntax comparison

[他言語でのmatch構文](https://gist.github.com/iluuu1994/11ac292cf7daca8162798d08db219cd5)。

## Vote

投票は2020/07/03まで、投票者の2/3の賛成で受理されます。
2020/06/22時点では賛成20反対1となっていて、よほどの問題でも発生しないかぎり受理されるでしょう。

# 感想

switchでよく問題になっていた曖昧な比較やbreakし忘れといったミスが、構文レベルで不可能となります。
そのため、match式に従っておけばswitchに起因する問題はほぼ発生しなくなるでしょう。
またmatch全体が返り値を持ってるのも便利ですね。

そのかわり、case内部には1式しか書けないため、複数の変数値を変更したり入れ子にしたりといった[複雑な処理](https://github.com/php/php-src/blob/php-7.4.0/ext/iconv/iconv.c#L1500)を書くことは難しくなります。
また、あえてbreakを書かずに継続したい場合も面倒な書き方になります。

```php
// 1ならfooとbarを、2ならbarだけ実行したい
switch($x){
	case 1:
		foo();
	case 2:
		bar();
		break;
}

// after
match($x){
    1 => foo() && bar(),
    2 => bar(),
};
```

`{}`で括って複数の文を書けるようにするかどうかは、[アロー関数](https://qiita.com/rana_kualu/items/7fbdd520a7e355599f7d#multi-statement-bodies)同様今後の課題となっています。

従って本RFCは、決してあらゆるswitchを置き換える構文ではなく、アロー関数のように一部のswitch文を置き換えることができる短縮構文という立ち位置になります。
しかし、よほど変なことでもしていないかぎり、大抵のswitch文はmatch式に置き換えることができると思います。
安全性のためにも、今後はできるだけmatch式を使っていくとよいでしょう。