Rust パターンマッチの網羅性

Rustのパターンマッチは網羅性が検査され、網羅的でない場合はコンパイルエラーになる。網羅性は以下のように検証される。

型の分類

パターンマッチの網羅性をするときには、全ての型がADTのように扱われる。つまり、有限個の引数をとるコンストラクタが有限個あり、そのいずれかにより生成されていると仮定して、網羅性が判定される。

constructor_arity あたりを読むとわかるが、例えば、

  • 通常のADTは、そのままその意味でADTとみなされる。
    • ただし、空なADTであっても、空であることが今いるモジュールからわからない場合は、余分なコンストラクタを持っているとみなされる。
  • booltruefalse の2つのコンストラクタからなるとみなされる。
  • u8i32 などはオープンな型とみなされ、全ての定数を網羅しても網羅的とはみなされない。これを修正する提案がある
  • 参照は単一のコンストラクタを持つものとみなされる。
  • 固定長配列は単一のコンストラクタを持つものとみなされる。
  • スライスは長さごとに別々のコンストラクタを持つものとみなされる。ただし、計算の都合上、ある長さより長いものはまとめて扱われる。

パターンは以下のどれかに分類される

  • ワイルドカードに相当するパターン ( _ や識別子への束縛)
  • 単一のコンストラクタに対応するパターン (&p(p1, p2) など)
  • 複数のコンストラクタに対応するパターン (スライスパターンの一部)

複数のコンストラクタに対応するパターンはあまり多くないので、簡単のため以下ではなかったことにする。

アルゴリズム

Rustのパターンマッチの網羅性は is_useful 関数に帰着される。 is_useful 関数は以下の問題を解く。

入力:

  • パターンからなる n×m行列 M
  • パターンからなる 1×mベクトル v

出力:

  • m個の値の組で、Mにマッチせずvにマッチするものがあるか否か

この関数は、マッチの各腕の到達可能性にも使われるし、網羅性検証にも使われる。到達可能性ならば

  • m = 1
  • M = ある腕より前にある腕のうち、ガードのないもの
  • v = 該当の腕

とし、網羅性検証ならば

とおけばよい。

is_useful は以下のように解かれる。

  • m = 0, n = 0 のとき、true
  • m = 0, n > 0 のとき、false
  • m > 0 のときは、最初の列に注目して処理をする。
  • v0がコンストラクタならば、これに基づいて以下の処理をする。
    • Mの各行を、v0のコンストラクタにより特殊化する。v0のコンストラクタのarityをkとすると、新しい行列は(m-1+k)列になる。行数はn以下になる。
    • v自身も同様に特殊化する。
    • 特殊化したMとvに対して、 is_useful を実行する。
  • v0がワイルドカード相当ならば、
    • v0の型から考えられるコンストラクタと、Mの左端の列に出現するコンストラクタを比較する。
    • 網羅されていないコンストラクタがある場合は、true
    • 全てのコンストラクタが網羅されている場合は、v0の全てのコンストラクタについて、特殊化した is_useful を試す。

空な型への対応

空(uninhabited)な型とは、値を作ることができないことがわかる型である。

空な型は #![feature(never_type)] の有無により挙動が異なる。

#![feature(never_type)] がない場合

原則として、型は非空なものとして扱われる。ただし、マッチの腕が0個の場合は、以下のいずれかの場合に網羅的とみなされる。

  • マッチ対象の型が ! である。
  • マッチ対象の型が列挙型で、その列挙型がバリアントをひとつも持っていない。

例えば、以下の例では #![feature(never_type)] を外すとコンパイルが通らない。

#![feature(never_type)]

enum Empty {}

fn f(x: (Empty,)) {
    match x {};
}

fn main() {}
#![feature(never_type)]

enum Empty {}

fn f(x: Option<Empty>) {
    match x {
        None => {},
    }
}

fn main() {
    
}

#![feature(never_type)] がある場合

#![feature(never_type)] がある場合、空な型は以下のいずれかである

  • !
  • 空なフィールドを持つタプルや構造体
  • 空なフィールドからなる共用体
  • 全てのバリアントが空なフィールドを持つような列挙型
  • 空な要素型を持つ要素数1以上の配列
  • 空な型への参照

例外として、フィールドや型がmatchのあるモジュールから不可視の場合は、その構造体は非空と仮定される。

まとめ

  • パターンマッチの網羅性は、およそ期待された通りにチェックされる。ただし、以下の例外がある。
  • 整数型の網羅性は適切にチェックされない。
  • 空な型のパターンマッチングは現在のところかなり保守的に検査される。ただし、 #![feature(never_type)] を使うと、より意味的に正しい方法で検査されることになる。