2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rustのmatch網羅性チェックに救われた話——Schemeインタプリタを書きながら気づいたこと

2
Last updated at Posted at 2026-04-30

はじめに

Rust の match は「全てのケースを網羅していないとコンパイルエラーになる」という性質があります。初心者向けの解説ではよく「型安全性の一例」として紹介される機能ですが、実際にこれが効いてくる場面を自分の手で実装していくことで、じっくりと体感することができました。

これは Scheme インタプリタを Rust で書いていたときに、コンパイラに助けられた話です。「ああ、これは単なるコンパイラの優しさじゃなくて、バグを未然に防ぐ安全装置だったんだ」と腑に落ちた瞬間の記録です。

舞台:Scheme の「値」を Rust の enum で表現する

Scheme の値にはいくつかの種類があります。数値、文字列、真偽値、リストなど。これを Rust の enum で素直に表現できます。

#[derive(Debug, Clone)]
enum Value {
    Number(f64),
    Str(String),
    Bool(bool),
    List(Vec<Value>),
}

この Value 型の変数には、4つのうちどれか1つしか入っていないことが 型レベルで保証 されます。Python の辞書やリストに何でも詰め込むときの「これ本当に何が入ってるんだっけ?」という不安とは、もう無縁になれます。

これらの値を扱う関数は、match で場合分けすれば簡単に書けます:

fn to_string(v: &Value) -> String {
    match v {
        Value::Number(n) => n.to_string(),
        Value::Str(s)    => format!("\"{}\"", s),
        Value::Bool(b)   => b.to_string(),
        Value::List(xs)  => {
            let parts: Vec<String> = xs.iter().map(to_string).collect();
            format!("({})", parts.join(" "))
        }
    }
}

関数が増えても同じパターン。evaluate も、equal? も、display も、どれも Value の各バリアントを処理する match で書いていきます。

事件:クロージャを追加したくなった

処理系が育ってきて、関数オブジェクト(クロージャ) を値の一種として扱いたくなりました。lambda を評価すると、引数リストと本体とキャプチャした環境を持つ値が生まれる。これも Value の新しいバリアントとして足すのが自然です:

#[derive(Debug, Clone)]
enum Value {
    Number(f64),
    Str(String),
    Bool(bool),
    List(Vec<Value>),
    Closure {
        params: Vec<String>,
        body: Box<Expr>,
        env: Rc<RefCell<Env>>,
    },
}

バリアントを1つ足しました。これだけの変更です。他に触った場所はありません。

さて、保存して cargo build

コンパイルエラーの嵐(ただし、いい意味で)

出てきたのはこんなエラーでした:

error[E0004]: non-exhaustive patterns: `Closure { .. }` not covered
 --> src/main.rs:45:11
   |
45 |     match v {
   |           ^ pattern `Closure { .. }` not covered

しかも、同じエラーが Value を match している全ての箇所で出ています。to_stringevaluateequal_valuesdisplay_value、それぞれ違うファイル・違う関数で、コンパイラが全部指摘してくれる。

error[E0004]: non-exhaustive patterns: `Closure { .. }` not covered
 --> src/eval.rs:112:11
error[E0004]: non-exhaustive patterns: `Closure { .. }` not covered
 --> src/display.rs:23:11
error[E0004]: non-exhaustive patterns: `Closure { .. }` not covered
 --> src/equal.rs:8:11

これは怒られているのではなく、手厚いガイダンスです。「Closure を足したけど、この関数、この関数、この関数で処理を書き忘れていませんか?」と、全部の現場を網羅的に教えてくれている。修正の方針が明確で、指摘された箇所を順に埋めていけば、抜け漏れなく対応できます。

もし他の言語だったら

この手のバグが、他の言語ではどう顕在化するかを考えると、Rust のありがたみが際立ちます。

Python の場合

class Value:
    pass

class Number(Value): pass
class Str(Value): pass
class Closure(Value): pass  # ← 新しく追加

def to_string(v):
    if isinstance(v, Number):
        return str(v.n)
    elif isinstance(v, Str):
        return f'"{v.s}"'
    # Bool、List、Closure の分岐が無い → Noneが返る

Closure を渡すと、静かに None が返ってくるだけ。単体テストを網羅的に書いていなければ、リリースするまで気づかない。

TypeScript の場合

switch 文では網羅チェックされない(never 型のトリックを使えば疑似的にできるが、言語組み込みではない)。デフォルト分岐を忘れると、未定義値のまま処理が続く。

Rust の場合

コンパイルが通らない。そもそも実行できない。バグが実行時まで到達しない。

小さなコメントだけど、安全性の哲学

non-exhaustive patterns は、初めて見たときは「なんでこのエラーが出るの、面倒だな」と思うかもしれません。でも、enum でドメインを型レベルで表現して、match でそれを処理するスタイルで書くと、このエラーが「抜け漏れを指摘してくれる守護神」に変わります。

特に Scheme のような 値の種類が明確に決まっている 対象を扱うときは効きます。新しい値の種類を追加するたびに、全ての処理関数で考慮漏れがないかコンパイラが保証してくれる。リファクタリング時の安心感が、他の言語とは違う質のものになります。

まとめ

  • Rust の enum + match は「場合分けの網羅性」を型レベルで強制する
  • バリアントを追加したら、それを使う全ての matchnon-exhaustive patterns エラーが出る
  • これはコンパイラからの親切なガイダンス。修正箇所を網羅的に教えてくれる
  • 他の言語では静かに None が返ったり、未定義の挙動になったりする類のバグが、実行前に全部検知される
  • enum + match はデータドメインを型で表現し、その処理を網羅的に書くための強力な組み合わせ

Scheme インタプリタのように 値の種類を列挙できる領域 を扱うコードでは、この性質が特に効いてきます。最初は少し窮屈に感じるかもしれませんが、育ってきたコードベースに変更を加えていくときmatch 網羅性チェックは本当に頼りになります。


参考

この記事は、私が書いた技術書『Rustで作るSchemeインタプリタ』の実装過程で感じたことから生まれました。書籍では、約1,000行の Scheme 処理系を Rust で構築する過程で、enummatch を使った値の表現、評価器の構造、evalapply の相互再帰などを扱っています。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?