Python3 に提案されたばかりのパターンマッチング構文を調べてみた

追記: この記事から8ヶ月後に、パターンマッチ構文は Python に採用されることが決まりました。 [PEP 634, 635, 636 (Structural Pattern Matching) を読んだよメモ](https://qiita.com/tk0miya/items/d9f816b14101ff83d5b7) では最新情報をもとに記事を再構成しています。


昨夜、突如 [PEP 622 (Structural Pattern Matching)](https://www.python.org/dev/peps/pep-0622/) が [python/peps に投稿されました](https://github.com/python/peps/pull/1451)。
python 系のメーリングリストを追いかけていない僕にとっては突然の大物 Draft の登場で、大混乱です。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">野心的な PEP が出てきた。なにこれやばい。あとでじっくり読む。 / PEP 622 -- Structural Pattern Matching | <a href="https://t.co/HIipocP5Vt">https://t.co/HIipocP5Vt</a> <a href="https://t.co/o0OUN6BqgH">https://t.co/o0OUN6BqgH</a></p>&mdash; tk0miya (@tk0miya) <a href="https://twitter.com/tk0miya/status/1275467583437680641?ref_src=twsrc%5Etfw">June 23, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

というわけで、今回は PEP 622 を読んでみようと思います。

なお、昨日投稿されたばかりのできたてほやほやで、まだ Draft ステータスですから採択されるかどうかはわかりませんし、もし採択されたとしてもどういう形になっているかはわかりませんので、与太話として読んでいただくのが良いかと思います。

(注: PEP 622 のうち、使い方に関する部分についてまとめたものです。実装に近い部分、静的解析などに関する部分はここでは扱いません)

## 概要

* Python のコードベースを調査したところ、`isinstance()` がよく使われていることがわかった (`len()` に続いて 2位)
* 多くのケースでは、その直後に属性アクセスが行われていた
* これは多くのプログラムでは、**heterogeneous data** (ある変数が複数のデータ型を取りうること)が登場することを示している
* また、Python にはオブジェクトを分解してデータを取り出すよい方法がない [^1]
* Python にパターンマッチング構文を追加し、heterogeneous dataを扱いやすくする

[^1]: "Python doesn't have expressive ways of destructuring object data (i.e. separating the content of an object into multiple variables)." よい説明があれば置き換えたい…

## アプローチ

新しいパターンマッチング構文は match と case というふたつのキーワードを用います。

```python
match shape:
    case Point(x, y):
        ...
    case Rectangle(x, y, _, _):
        ...
```

`match` に指定した値が、各 `case` 句のいずれかにマッチするかどうかを順にチェックしていきます。
後述しますが、else 句はありません。

case 句に指定できる条件には、以下のものがあります。

* Literal Pattern
* Name Pattern
* Constant Value Pattern
* Sequence Pattern
* Mapping Pattern
* Class Pattern

ここでは例とともに各パターンを紹介します。

### Literal Pattern

リテラル値を使ったパターンです。文字列、数値、boolean、None が使えます (ただし f-string は除く)。

```python
match number:
    case 0:
        print("Nothing")
    case 1:
        print("Just one")
    case 2:
        print("A couple")
    case -1:
        print("One less than nothing")
    case 1-1j:
        print("Good luck with that...")
```

(Python にはありませんが) switch 文みたいなものですね。

### Name Pattern

case 句に変数名を指定すると、常にマッチは成功し、同時に値をキャプチャします[^2]。

[^2]: 原文では assignment (代入)と読んでいますが、ここでは理解しやすさを優先してキャプチャと呼ぶことにしました。他の言語だと変数束縛(variable binding)と呼ばれたりもします。

以下の例では、変数 `greeting` の値が `name` に代入されます。

```python
match greeting:
    case name:
        print(f"Hi {name}!")
```

Name Pattern は常にマッチに成功してしまうため、条件分岐には役立ちません。
他のパターンと組み合わせることで役立ちます。

なお、同じ case 句の中で同じ変数名を二度以上使うことはできません。

```python
match data:
    case [x, x]:  # Error!
        ...
```

### Name Pattern (亜種)

case 句に `_` を指定した場合、Name Pattern と同様に常にマッチに成功します。
しかし、値のキャプチャは行われません。

```python
match greeting:
    case _:
        print(f"Hi {_}!")  # Matched, but error because not captured!
```

この `_` という記号は Rust や Scala と似た書き方とのことです。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Rustのmatchも_で何にでもマッチするのでそれに近い感じですかね。<a href="https://t.co/WxcGESxh0J">https://t.co/WxcGESxh0J</a></p>&mdash; Hiroaki Nakamura ⋈ (@hnakamur2) <a href="https://twitter.com/hnakamur2/status/1275799285813006336?ref_src=twsrc%5Etfw">June 24, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


また、他の Name Pattern の変数と異なり、`_` は同じ case 句の中で複数回指定することができます。

```python
match data:
    case [_, _]:
        print("Some pair")
```

case の並びの最後に `case _:` を置くと else っぽく使える、という説明がありました。

### Constant Value Pattern

定数や Enum を使ったパターンです。
case 句にドット付きの名前を渡した場合は Name Pattern ではなく Constant Value Pattern とみなされ、値を使った比較が行われます。
グローバルレベルの変数をパターンとして指定する場合は先頭にドットを付けます (ドットを付けないと Named Pattern になってしまいます)。

```python
from enum import Enum

class Color(Enum):
    BLACK = 1
    RED = 2

BLACK = 1
RED = 2

match color:
    case .BLACK:
        print("Black suits every color")
    case Color.BLACK:
        print("Black suits every color")
    case BLACK:  # This will just assign a new value to BLACK.
        ...
```

### Sequence Pattern

シーケンス系のデータ(`collections.abc.Sequence` のサブクラス [^2])にマッチするパターンです。
list もしくは tuple の記号を使って書きます。

[^2]: イテレータは含まれないので注意

```python
match collection:
    case [1, 2, 3]:
        print("Got 1, 2, 3")
    case (4, 5, 6):
        print("Got 4, 5, 6")
```

これに先ほどの Name Pattern を組みわせると、複雑なデータ型にマッチするパターンを記述できます。
最初のパターンは 2要素のシーケンスで、先頭が 1、次が任意の値にマッチします(そして2番目の要素は変数 `x` にキャプチャされます)。
二番目のパターンは 2要素のシーケンスで、先頭が 1、次が 1件以上のデータを持ったシーケンスにマッチします(そして、先頭の要素は `x` に、後続の要素は `others` にそれぞれキャプチャされます)。

```python
match collection:
    case (1, x):
        print(f"Got 1 and {x}")
    case 1, [x, *others]:
        print("Got 1 and a nested sequence")
```

応用例として以下が挙げられています。

* `[*_]` ... 任意の長さのシーケンス (0件以上)
* `(_, _, *_)` ... 2件以上の長さのシーケンス
* `["a", *_, "z"]` ... 先頭が `"a"`、末尾が `"z"` である、任意の長さのシーケンス

### Mapping Pattern

マッピング系のデータ(`collections.abc. Mapping ` のサブクラス)にマッチするパターンです。
dict の記号を使って書きます。

最初のパターンは `"route"` というキーを持つマッピングにマッチします (そして値は変数 `route` にキャプチャされます)。
二番目のパターンは `constants.DEFAULT_PORT` というキーを持つマッピングにマッチします (そして値は `sub_config` に、残りのデータは `rest` にキャプチャされます)。

```python
import constants

match config:
    case {"route": route}:
        process_route(route)
    case {constants.DEFAULT_PORT: sub_config, **rest}:
        process_config(sub_config, rest)
```

なお、このパターンでは **指定したキーがマッピングに含まれるかどうか** だけで判定を行います。
言い換えるとその他のキーが含まれていても、条件にはマッチします。
例えば `{"route": 1, "key": 2}` というデータは最初のパターンにマッチします。

なお、 `__missing__` や `__getitem__` などによってデータを生成するタイプのデータ構造には対応しないため、defaultdict などはマッチしない可能性があります。

### Class Pattern

クラス系のデータにマッチするパターンです。
指定したクラスのオブジェクトであることを判定し、属性値が一致するオブジェクトにマッチします。

最初のパターンは `Point` クラスのオブジェクトにマッチします (そして、属性はそれぞれ `x`, `y` にキャプチャされます)。
二番目のパターンは `Rectangle` クラスのオブジェクトにマッチします (以下省略)。

```python
match shape:
    case Point(x, y):
        ...
    case Rectangle(x0, y0, x1, y1, painted=True):
        ...
```

## その他

### パターンの組み合わせ

`|` を使うことで、複数の条件を併記できます。

```python
match something:
    case 0 | 1 | 2:
        print("Small number")
    case [] | [_]:
        print("A short sequence")
    case str() | bytes():
        print("Something string-like")
    case _:
        print("Something else")
```

なお、併記されたパターンで Name Pattern を利用する場合、各パターンで同じ変数名を使わなくてはなりません。

```python
match something:
    case 1 | x:  # Error!
        ...
    case x | 1:  # Error!
        ...
    case one := [1] | two := [2]:  # Error!
        ...
    case Foo(arg=x) | Bar(arg=x):  # Valid, both arms bind 'x'
        ...
    case [x] | x:  # Valid, both arms bind 'x'
        ...
```

### ガード条件

case 句の後ろに if 文を加えることでガード条件を加えることができます。
Name Pattern による変数のキャプチャを付け加えることで、特定のデータ構造に一致するもので、特定の条件を満たす場合にマッチするというパターンが記述できます。

```python
match input:
    case [x, y] if x > MAX_INT and y > MAX_INT:
        print("Got a pair of large numbers")
    case x if x > MAX_INT:
        print("Got a large number")
    case [x, y] if x == y:
        print("Got equal items")
    case _:
        print("Not an outstanding input")
```

### Named sub-patterns

Walrus operator を使うことでサブパターンを表現できます。

```python
match get_shape():
    case Line(start := Point(x, y), end) if start == end:
        print(f"Zero length line at {x}, {y}")
```

### match と case は soft keywords 扱い

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">match ってキーワード、いろんなプログラムとぶつかってギャッとなりそう。大丈夫なんかな。</p>&mdash; tk0miya (@tk0miya) <a href="https://twitter.com/tk0miya/status/1275469998312394752?ref_src=twsrc%5Etfw">June 23, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

最初、match と case をキーワードにするのかと読み違ってぎょっとしましたが、勘違いでした。

>The match and case keywords are proposed to be soft keywords, so that they are recognized as keywords at the beginning of a match statement or case block respectively, but are allowed to be used in other places as variable or argument names.

これまでどおり変数や引数などに利用できます。`re.match` も今まで通り使えます。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">パーサーに詳しくないので推測ですが、多分パーサーが新しくなった影響でしょうね。<br><br>今までは新しいキーワードを追加したら（多少の時間差はあれど）予約語になって変数名とか関数名に使えなくなってました。それが async であちこちに迷惑かける事になってしまった。</p>&mdash; Inada Naoki (@methane) <a href="https://twitter.com/methane/status/1275793979116621825?ref_src=twsrc%5Etfw">June 24, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

3.9 から導入された PEG Parser のおかげのようです (PEP 622 の冒頭にも一言触れられてる)。

### `__match__()` プロトコルについて

Class Pattern の実現には `__match__()`、 `__match_args__` という特殊メソッド、特殊属性を使うとのことです。
…が、説明を読んでもぱっと理解できなかったので、TODO となっているサンプルの登場を待つことにします。

自分で作成したクラスをマッチングさせるときに、特殊な対応が必要な場合に役立つかもしれません。

## 感想

* Erlang や Ruby の "あの" パターンマッチングが登場するのか、という驚きがあった
* JSON を始めとする複雑なデータ構造を、シンプルでパワフルな記述でマッピング & 分解できるとしたら本当に素晴らしい
    * [Ruby 2.7.0 のリリースノート](https://www.ruby-lang.org/ja/news/2019/12/25/ruby-2-7-0-released/) に載っているサンプルを見ると、パワフルさが伝わるのではないかと思います
    * これをパターンマッチなしで書くのは結構面倒ですよね
* cpython 向けの実装はおおよそできているとのことなので、早く試してみたい (記事は書いたものの動かせていない)
* まだ Draft 段階で、採用は決まっていないのだけど 3.10 (3.11?)が楽しみです