6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Let chains実装記念】パターンマッチテクニック集【Rust】

Last updated at Posted at 2025-07-15

次の書き方に違和感を持った方はぜひ本記事を読んでみてほしいです!

  • let Point { x, y } = p;
  • let c @ 'A'..'z' = v else { return; };
  • let () = {};
  • let ((Some(n), _) | (_, n)) = (opt, default);

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=b21737023e469242ce2676091a91e4ca

読み終わる頃には理解できるはず!(多分 :sweat_drops: )

先月、 Rust 1.88 がリリースされました :cracker: :cracker:

特に「 if let 式で let&& で繋げられる」 Let chains というのが目玉機能として挙げられています1

Let chains
fn jsons_json_in_json_article_checker(contents: &str) -> anyhow::Result<bool> {
    // if let 式
    if let Contents {
        author,
        content: Content::Other { body: json_str },
        ..
    } = serde_json::from_str(contents)?
    // 1.88からは、さらに条件をくっつけられる!
    && author == "Json"
    // 1.88からは、let もくっつけられる!
    && let JsonInJsonContent::Article { body } = serde_json::from_str(&json_str)? {
        println!("It's Json's Article: {}", body);
        Ok(true)
    } else {
        Ok(false)
    }
}
全体
Rust
#![allow(unused)]

use serde::Deserialize;

#[derive(Deserialize)]
struct Contents {
    id: usize,
    author: String,
    content: Content,
}

#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum Content {
    Article {
        title: String,
        body: String,
    },
    Other {
        body: String,
    },
}

#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum JsonInJsonContent {
    Article {
        body: String,
    },
    Other {
        body: String,
    },
}

fn jsons_json_in_json_article_checker(contents: &str) -> anyhow::Result<bool> {
    if let Contents {
        author,
        content: Content::Other { body: json_str },
        ..
    } = serde_json::from_str(contents)?
    && author == "Json"
    && let JsonInJsonContent::Article { body } = serde_json::from_str(&json_str)? {
        println!("It's Json's Article: {}", body);
        Ok(true)
    } else {
        Ok(false)
    }
}

fn main() -> anyhow::Result<()> {
    let contents = r#"{
    "id": 13,
    "author": "Json",
    "content": {
        "type": "other",
        "body": "{ \"type\": \"article\", \"body\": \"Today is Friday.\" }"
    }
}"#;

    let true = jsons_json_in_json_article_checker(contents)? else {
        println!("Pattern matching failed...");
        return Ok(());
    };
    
    Ok(())
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=9ff3d4246ae2aeea4ac956ebd3409734

とても嬉しい機能が追加されたので、 let-else文のノリで 記事を書こうかと最初は思ったのですが、素晴らしい 先達記事 がありましたので本記事では if let 式の周辺知識、という位置付けでパターンマッチのテクニック集を紹介したいと思います!

基礎事項についてはほぼ公式ドキュメントのまとめ直しです。

一次ソースに当たりたい方は以下も見てみてください!

Rustのパターンマッチ

if let 式の let 部分ではRustのパターンマッチ機能が利用できます。

本節ではそもそもパターンマッチってなんだっけ...?というところからおさらいしたいと思います。


基本としては「 match 式の左側に列挙するもの」という認識で大体あっています。

他の言語でもあるように列挙体で分岐する場合もあれば...

列挙体での分岐
#[derive(Clone, Copy)]
enum Fruits {
    Apple,
    Banana,
    Orange,
}

impl Fruits {
    fn get_price(self) -> u32 {
        match self {
            Fruits::Apple => 200,
            Fruits::Banana => 100,
            Fruits::Orange => 250,
        }
    }
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=0eb49ac4a992e7a26b09d49d3e8d8182

プリミティブ型2の値で分岐する場合もあります。

プリミティブ型での分岐
let age = 10;

let s = match age {
    0 => "生まれたばかり",
    1 | 2 => "1, 2歳",
    3..=6 => "幼稚園",
    7..=15 => "義務教育",
    16..=18 => "未成年",
    _ => "成人",
};

println!("あなたの現在: {s}"); // 義務教育

let chr = '@';

match chr {
    'a' | 'A' => println!("エー"),
    c @ 'A'..'z' => println!("{c} はアルファベット"),
    c if c.is_ascii() => println!("{c} はアスキー範囲の文字"),
    c => println!("その他: {c}"),
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=e3b8e70e63810a662cab396dbb028629

もっと面白い例もあるのですがネタが尽きてしまうので記事の後半で...

この先 match 式に限らない色々な構文を紹介していく予定ですが、その前にパターンマッチおよび "パターン" について解説します!

パターンマッチの役割: 「分解」「束縛」「合致判定」

パターンマッチには大体3つの役割があります。

まず2つは「分解」と「束縛」です。TSなどにもある「分割代入」と同じ機能ですね。

分割代入の例
fn get_current_location() -> (f64, f64) { /* 省略 */ }

let (x, y) = get_current_location();

この let 文では、 get_current_location の返り値を (x, y) というタプルに分解しています。ついでに、 x , y という変数に束縛され、以降同スコープ内で変数 xy が使えるようになっています。

分解はタプルの他にも構造体・列挙体・配列など色々な構文要素で可能です!

Rust
#[derive(Clone, Copy, Debug)]
struct Point {
    x: f64,
    y: f64,
}

fn main() {
    // 通常の変数宣言
    let p = Point { x: 0., y: 10. };

    let Point { x: a, y: b } = p; // 分解して変数 a, b に束縛
    println!("{a} {b}");

    // 変数名がフィールド名と同じで良い場合
    let Point { x, y } = p;

    println!("{x} {y}");
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=33ed488bc9638646b00844241d3f78bc

隙有場合合致すきあらばパターンマッチ

ここで逆転の発想として押さえておいてもらいたいのは、Rustで 「変数の束縛(あるいは変数宣言)ができるならその箇所はパターンマッチである」 という事実です。

以下は後述の論駁不可能パターンしか取れませんが全て「パターンマッチチャンス :sparkles::sparkles: 」なのです!

パターンマッチチャンス特集
#[derive(Clone, Copy, Debug)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
    fn dist(
        self,
        // 関数の引数でパターンマッチ!
        Point { x: x1, y: y1 }: Point,
    ) -> f64 {
        // もちろんletでパターンマッチ!
        let Point { x: x0, y: y0 } = self;

        ((x0 - x1).powi(2) + (y0 - y1).powi(2)).sqrt()
    }
}

fn main() {
    // ただの束縛 (でもパターンマッチの一つと見れる!)
    let points = [
        Point { x: 0., y: 0. },
        Point { x: 1., y: 2. },
        Point { x: 2., y: 3. },
        Point { x: 5., y: 0. },
        Point { x: 0., y: 0. },
    ];

    let mut total_dist = 0.;
    let mut pre_p = points[0];
    // for でもパターンマッチ!
    for &p @ Point { x, y } in &points[1..] {
        println!("now: ({x}, {y})");

        total_dist += p.dist(pre_p);

        // let がない時は流石に束縛(もとい再代入)しかできない
        pre_p = p;
    }
    
    let total_dist_1 = points[1..]
        .iter()
        .fold(
            (0., points[0]),
            // クロージャ引数でもパターンマッチ!
            |(total, pre_p), &p| (p.dist(pre_p) + total, p)
        ).0;

    println!("{total_dist}, {total_dist_1}");
    assert_eq!(total_dist, total_dist_1);
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=16ba58a7ef779e85261e37971ffd2236

恣意的な例で少し冗長ですが、上記ソースコードでは以下のパターンマッチが登場しています。

  • 関数引数でのパターンマッチ: Point { x: x1, y: y1 }: Point
  • let 文でのパターンマッチ: let Point { x: x0, y: y0 } = self;
  • for 文でのパターンマッチ: for &p @ Point { x, y } in &points[1..] {...}
  • クロージャ引数でのパターンマッチ: |(total, pre_p), &p| {...}

本記事後半ではパターンマッチが使える事例を個別に一応紹介していこうと考えていますが、 そもそも変数束縛ができる箇所は大体パターンマッチだ という風に捉えておいた方が自然に見えるんじゃないかと思います。

ただし、以下に示す様な例外もあります。

  • let がないミュータブル変数への再代入
  • static / const による定数宣言

合致判定

最後の役割が名前通りパターンにマッチするかどうかの検証です。ただし、この役割は後述の「論駁可能パターン」のみが持ちます。

Rust
let mut v: Vec<usize> = (1..10).collect();

while let Some(n) = v.pop() { // パターンにマッチしている間のみ実行
    match n {
        m if m % 2 == 0 => println!("{m}は偶数"),
        m => println!("{m}は奇数"),
    }
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=380be8eac08b1d6735111090b17b5a0e

ここで示した while let 文や match 式は論駁可能パターンによるパターンマッチで、論駁可能パターンの特権である「パターン合致検証」を行ない、合致する時のみスコープ内の文を実行しています。これらのパターンマッチも、前節で紹介した「分解」と「束縛」の役割も担っている点にも注目です。

まとめ: パターンマッチの役割

  • 分解
  • 束縛
  • 合致判定

論駁可能パターンと論駁不可能パターン

参考: 論駁可能性: パターンが合致しないかどうか

ここまでの説明ですでに何度か顔を出していましたが、パターンマッチのパターンは、論駁(可能|不可能)パターンの2つに分類されます!

  1. 論駁可能パターン ( refutable pattern )
  2. 論駁不可能パターン ( irrefutable pattern )

なぜ「論駁」?

論駁というのは「反論」と大体似た意味の言葉です。 refutable という英単語が割り当てられているのですが、それの直訳が「論駁」なので和訳ドキュメントは論駁なのだろうと思います。

...かっこいいからヨシ!

論駁可能パターン

受け入れ・束縛に失敗することがある パターンマッチのパターンを論駁可能なパターンといいます。 match 式のアームや if let 式に用いられるパターンは(基本的に3)論駁可能パターンです。

論駁不可能パターンたち
#[derive(Clone, Copy, Debug)]
enum Fruits {
    Apple,
    Banana,
    Orange,
}

struct PurchaseResult {
    fruits: Option<Fruits>,
    change: u32,
}

impl Fruits {
    fn get_price(self) -> u32 {
        match self {
            // Appleじゃないこともある
            // よって論駁可能パターン
            Fruits::Apple => 200,
            
            // Bananaじゃないことも当然ある
            // 論駁可能パターン
            Fruits::Banana => 100,

            // Orangeに当てはまるとも限らない
            // 論駁可(ry
            Fruits::Orange => 250,
        } // ただしどれかでは絶対ある
    }

    fn try_purchase(self, payment: u32) -> PurchaseResult {
        let price = self.get_price();
        if payment >= price {
            PurchaseResult {
                fruits: Some(self),
                change: payment - price,
            }
        } else {
            PurchaseResult {
                fruits: None,
                change: payment
            }
        }
    }
}

fn main() {
    let res = Fruits::Banana.try_purchase(300);

    // 買えない時もあればお釣りが0円の時もあり、以下のパターンマッチは失敗する可能性がある
    // 論駁可能パターン
    if let PurchaseResult { fruits: Some(f), change: ch @ 1.. } = res {
        println!("{f:?} が購入できてお釣りは {ch} 円だった!");
    }
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=9d2c7b8d3325297f7a6002f68de35acd

Fruits::ApplePurchaseResult { fruits: Some(f), change: ch @ 1.. } は常に受け入れられる・束縛できるとは限らないので、全て論駁可能パターンというわけです。

PurchaseResult {...} の例を見るとわかる通り、論駁可能かどうかはパターンの複雑さには特に関係なく、純粋に 「常に受け入れ・束縛可能かどうか」 のみで決まります。

補足: 「束縛」ではなく「受け入れ・束縛」と書いたのは、 Fruits::Apple のように変数が一切生まれないパターンもあるためです。広義的にこれを束縛と捉えても良いかも...?

別な例
#[derive(Clone, Copy)]
struct Point {
    x: u32,
    y: u32,
}

fn main() {
    let point = Point { x: 10, y: 20 };

    // こちらは後述の論駁不可能パターン
    let Point { x, y } = point;

    println!("x = {}, y = {}", x, y);

    // xが常に0とは限らないので、論駁可能パターン
    // let Point { x: 0, y } = point;
    /* refutable pattern in local binding: `Point { x: 1_u32..=u32::MAX, .. }` not covered
       `let` bindings require an "irrefutable pattern", ...
       と怒られる
    */
    // if letやmatchなら論駁可能パターンを取れる
    if let Point { x: 0, y } = point {
        println!("x = 0, y = {}", y);
    }
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=5037c4fbca05887ad45c2ef1c341725d

論駁不可能パターン

どんな場面でも絶対に受け入れ・束縛に成功する パターンマッチを論駁不可能なパターンと言います。

通常の変数束縛はある意味で絶対成功するパターンマッチです。その他にも、構造体の分解なども必ず成功するため論駁不可能パターンとして利用可能です。

通常の let 文が取れるパターンは論駁不可能パターンである必要があります。逆に言えば 論駁不可能なら意外になんでも書くことができます。以下例です。

次のlet文は全部コンパイルOK
// 普通の変数宣言
let p = Point { x: 0., y: 0. };

// 分解構文用途
let Point { x, y } = p;

// 配列もいけます
let [a, b, c, ..] = [0, 1, 2, 3, 4];

// 以下は処理としてはnop
let 0..=255_u8 = 10; // u8 の変数は必ず0から255
let () = {}; // ユニット値は当然ユニット値に束縛できる

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=d484f2a7776da0f5ea80399716526d8c

パターンマッチ全体のテクニック

ここまででパターンマッチで必ず押さえておかなければならない基礎事項もとい直感については説明できたと思います。ここからは、いよいよパターンマッチのテクニック集を見せていきたいと思います!

:sparkles: テクニック1 :sparkles: _.. で値を無視する

分解構文的にパターンマッチを見た際、利用しないフィールドがある時もあるでしょう。 _.. はそんな時に利用できたりします!

Rust
#[derive(Clone, Copy, Debug)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
    fn dist_of_x(
        self,
        Point { x: x1, y: _ }: Point,
    ) -> f64 {
        let Point { x: x0, y: _ } = self;

        (x0 - x1).abs()
    }
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=efadb21cae114107df99102817c44274

上記の例では、 y の値は使用しないため _ としています。

.. を使えば、「以降のフィールドを無視」といったことも可能です。たくさんフィールドがある際などに便利です。

Rust
struct Settings {
    interval: Duration,
    repeat_num: usize,
    hensuumei: String,
    kanngaeruno: bool,
    mendoukusakunatta: char,
}

let Settings {
    interval,
    repeat_num,
    ..
} = s;

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=3e05d94680e2b47f0213c5d0724dc950

__x の違い

変数の接頭辞に _ をつけることで未使用変数として扱えます。

実はこれは let _ = ..._ とは挙動が少し違います。

  • _x実際に束縛は行われる、すなわち所有権が奪われたりする
  • _: 束縛すら行われない。所有権も奪われない

_ は特別な記法として覚えておいて損はないでしょう。

:sparkles: テクニック2 :sparkles: | でパターン連結(し論駁不可能にする)

| を使うことで複数パターンを結合することができます。match 式のアームでよく使える記法です!

Rust
let c = 'a';

match c {
    'a' | 'A' => println!("a か A"), // 'a' と 'A' のパターンを結合
    c => println!("{c}"),
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=daafcee9d8f44d5e22228c4fb2093d94

ところで、 let 文は論駁不可能なパターンしか用いることができないのでした。しかし、 論駁不可能なら文句なし なわけです。というわけで、実は次のソースコードは問題ありません!

最初か後ろをバインド
fn hoge(opt: Option<usize>, default: usize) {
    let ((Some(n), _) | (_, n)) = (opt, default);

    // ↑↓ 処理的には同じ

    let n = opt.unwrap_or(default);

    println!("{n}");
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=3d2279a2c4cd1b95936a0d0b8e0803b3

Some(n) みたいなものを利用して束縛を行う場合 if let 式や後述の let else 文が必要になりそうなものですが、次の2点を守っていれば論駁不可能なので例の様な let 文が書けます!

  • | でくっつけたパターンのどれかには該当するか
  • どのパターンに該当したとしても、同じ型の同じ変数(例では n: usize )が同じだけ用意されること

:sparkles: テクニック3 :sparkles: 整数型と char 型は .. / ..= で範囲指定できる

Rustでは 1..10 で1以上10未満、 1..=10 で1以上10以下、といった具合に範囲型を作ることができます。

実はパターンマッチにおいて、整数型と char 型(文字型のこと)のみこの記法を用いたパターンを書くことが可能です!

Rust
let chr = 'Z';

match chr {
    'A'..'z' => println!("アルファベット"),
    c => println!("その他: {c}"),
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=28b31299d50726731c57af16eb25a43f

ちなみに全パターンを網羅(exhaustive)する必要がある match 式のアームに使った場合、この記法も使ってもしっかりと網羅性チェックは行われるので安心です :thumbsup:

使用頻度は高くないと思いますが、知っておいて損はないでしょう。

:sparkles: テクニック4 :sparkles: @ で値を束縛しつつパターン検証

「ある値があるパターンにマッチするかを調べたい」、でも、「元の値を利用したい」、そんな時に利用できるのが @ です!

Rust
let age = 15;

match age {
    ..13 => println!("子供"),
    n @ 13..=19 => println!("{n} 歳はティーンエイジャー"),
    n => println!("{n} 歳は大人"),
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=650237afd0fec6ab1f6a06f8a1413830

上記では範囲指定でのみ使っていますが、列挙型を深いネストと共にパターンマッチさせる場合などにも便利だったりします。

Rust
enum Gender {
    Male,
    Female
}

struct Person {
    first_name: String,
    last_name: String,
    gender: Gender,
}

use Gender::*;

impl Person {
    fn greet(self) {
        match self {
            // パターンマッチで男性であることを確かめつつ、 p に全体を入れる
            p @ Person { gender: Male, .. } => println!("Hello, Mr. {}", p.name()),
            p @ Person { gender: Female, .. } => println!("Hello, Ms. {}", p.name()),
        }
    }
    
    fn name(&self) -> String {
        format!("{} {}", self.first_name, self.last_name)
    }
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=b93f423a818eb6b80219936f8d936f3c

:sparkles: テクニック5 :sparkles: ref, ref mut, &

ref, ref mut, & を利用することで、参照化/参照外しが可能になります!

  • ref / ref mut: 変数の束縛時に、 ref なら不変参照、 ref mut なら可変参照とする
  • &: 評価される値が参照の時、参照を外した値にする ( Copy トレイト付与型を基本に捉えておいた方がよい)

refref mut は分割代入時に変数を参照化するのに使うと便利です。

ref / ref mut
fn count(mut self) -> Self {
    let Counter {
        ref mut counter,
        ref diff, 
    } = self;
        
    *counter += *diff;
    
    self
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=066543291d39c949c6bfdb82cf176434

とはいえ「ref / ref mut でないと絶対に実現できない処理」というのに筆者は出会ったことがなく、普通にRustを書いている分には必要としない機能かもしれません。

& の方はクロージャの引数でしばしば目にすることがあります。

Rust
fn main() {
    let v: Vec<usize> = (1..=10).collect();

    // iter だと v: &usize なので &v と書いて参照外し
    let s: usize = v.iter().map(|&v| v * v).sum();
    
    println!("{} = {s}",
        v.iter()
            .map(|i| format!("{i}*{i}"))
            .collect::<Vec<_>>()
            .join(" + ")
    );
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=50226a652aa49f6c4a7ab0864dfb93cf

イメージとしては &10 みたいな値がある時、 &10 に分解して v の方に 10 を束縛している感じでしょうかね...?

論駁不可能パターンが使える構文集

記事冒頭で「分割代入のあるところにパターンマッチあり」と紹介しました。とはいえ、結局具体的にどのような構文が使えるかの言及はしていなかったので、記事の残りではパターンマッチを使える構文集を論駁可能・不可能に分けて紹介していきたいと思います。

数が少ない & 基本的なものであるという理由から論駁不可能パターンからです。

let

let文
let 論駁不可能パターン = ;

もう何度も登場したお馴染みの束縛構文です。もはや説明不要!

let Point { x, y } = p;

良い機会なので「ちなみに」として書きますが、 let には変数宣言だけを行う機能もあります。

変な変数宣言たち
fn func(flag: bool) -> usize {
    let (val, _): (usize, usize);

    // val には一度だけ代入可能
    if flag {
        val = 10;
    } else {
        val = 20;
    }

    val * val
}

struct Point {
    x: u64,
    y: u64,
}

fn gunc() {
    let Point {
        x,
        y
    };

    x = 10;
    y = 10;

    println!("({}, {})", x, y);
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=136825ad188fe2a81dd97b3a84693fb6

例で書いた様に、パターンを利用して変数宣言を書くと非常にシュールですね... :sweat:

関数・クロージャの引数

関数・クロージャ
fn 関数名(論駁不可能パターン: ) -> 返り値型 {...}

|論駁不可能パターン| -> 返り値型 {...}

先に挙げた例の通り、関数やクロージャの引数部分でパターンマッチを行うことができます!

#[derive(Clone, Copy, Debug)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
    fn dist(
        self,
        // 関数の引数でパターンマッチ!
        Point { x: x1, y: y1 }: Point,
    ) -> f64 {
        // もちろんletでパターンマッチ!
        let Point { x: x0, y: y0 } = self;

        ((x0 - x1).powi(2) + (y0 - y1).powi(2)).sqrt()
    }
}

クロージャはともかく、関数引数部分での実用的な使い方例としては、Rustでオプショナル引数的なものを用意したくなった際、引数用構造体があると捗るのですが、その構造体を分解したい時に多少可読性が上がるかもという感じです。(未使用変数がわかるため)

struct FuncInput {
    arg1: usize,
    arg2: u32,
    arg3: u64,
    arg4: u128,
    arg5: i32,
    arg6: i64,
    arg7: i128,
}

impl Default for FuncInput {
    fn default() -> Self {
        Self {
            arg1: 10,
            // arg2 以降略
        }
    }
}

// ここで分解構文!
fn func(FuncInput {
    arg1,
    arg2,
    arg3,
    arg4,
    arg5,
    arg6,
    arg7,
}: FuncInput) {
    // 略
}

fn main() {
    // 引数が多い関数は引数用構造体を用意してDefaultをimplさせておくのが吉
    func(Default::default());
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=2b3260133a9b9d50e713d2da611dbd9a

クロージャの方はワンライナーにするためによく見かける気がします。

再掲
let total_dist_1 = points[1..]
    .iter()
    .fold(
        (0., points[0]),
        // クロージャ引数でパターンマッチ
        |(total, pre_p), &p| (p.dist(pre_p) + total, p)
    ).0;

for

for文
for 論駁不可能パターン in イテレータ {...}

for xxx in vvv {...}xxx 部分にパターンを書けます!

struct Point {
    x: usize,
    y: usize,
}

fn main() {
    let points = [
        Point { x: 0, y: 0 },
        Point { x: 1, y: 1 },
    ];
    
    for Point { x, y } in points {
        println!("x: {x} y: {y}");
    }
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=6a67f7765d3d0745ae79507226343e37

それだけなのですけども、 let 同様便利ですね。

特にイテレータに対して enumerate を呼んだ時などには、クロージャにしろ for 文にしろインデックスと値についてタプルでの受け取りが発生してしばしば書くんじゃないかと思います!

論駁可能パターンが使える構文集

残りは、論駁可能パターンを使う構文、すなわち条件分岐的な要素が入ってくる構文集です!論駁可能パターンを受け取る構文、不可能よりも結構ありますね :eyes:

match

match式
match  {
    論駁(可能|不可能)パターン => マッチした時の枝,
}

いわずもがな。パターンマッチといえば match 式、 match 式といえばパターンマッチですね。まとめていて気付きましたが、論駁可能パターン、不可能パターンの両方を活用する構文は地味にこの match 式ぐらいかもしれません。

この記事で示してきたように match アームもまた「同じ型のパターンであるなら何でもよい」わけですが、そのことを説明するたびに match 式を用いたRust版fizzbuzzをいつも連想するので、本記事でついでに紹介しておきたいと思います!

例 (fizzbuzz)
#[derive(Debug)]
enum FizzBuzzValue {
    FizzBuzz,
    Fizz,
    Buzz,
    Other(usize)
}

use FizzBuzzValue::*;

fn fizz_buzz(n: usize) -> FizzBuzzValue {
    match (n % 3, n % 5) {
        (0, 0) => FizzBuzz,
        (0, _) => Fizz,
        (_, 0) => Buzz,
        _ => Other(n)
    }
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=7f07a17db3ba0e962175858b1ab7e473

「15で割る → 5で割る → 3で割る」という順で確認する、というのがよくやる手だと思います。

一方で、上記のように (3で割ったあまり, 5で割ったあまり) のタプルでパターンマッチすることで、 3 * 5 = 15 みたいなワンクッションを置かなくても自然言語でのFizzBuzzのルールをそのままに条件分岐に落としこむことができています。

FizzBuzzだと実感が沸きにくいですが、直感的なパターンマッチを書きたい時、 match 式は強い味方になってくれるというわけです!

網羅性 (exhaustive)

match 式は論駁可能/不可能パターン両方を枝のパターンマッチに用いることができると書きました。

そんな match 固有の機能がいくつかあります。「パターンの網羅性(exhaustive)をチェックしてくれる」はその一つでしょう。

網羅性チェッカーは、 match 式のすべての枝をかき集めたときに論駁不可能であることを確認してくれます!

次のコードはコンパイルエラーになりません。

網羅的
let n: u8 = 5;
let m: u8 = 5;

let (b1, b2) = match (n, m) {
    (0..=128, 0..=128) => (false, false),
    (0..=128, 129..=255) => (false, true),
    (129..=255, 0..=128) => (true, false),
    (129..=255, 129..=255) => (true, true),
};

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=c57a605dfb7448748601c34ea1065424

一方で少しでも網羅性が崩れる(論駁可能である)とコンパイルエラーになります!

網羅的でない (コンパイルエラー)
let n: u8 = 5;
let m: u8 = 5;

let (b1, b2) = match (n, m) {
    (0..=128, 0..=128) => (false, false),
    (0..=128, 129..=255) => (false, true),
    (129..=255, 10..=128) => (true, false), // 129..=255, 0..=9 の時が抜けている
    (129..=255, 129..=255) => (true, true),
};
エラー内容
error[E0004]: non-exhaustive patterns: `(129_u8..=u8::MAX, 0_u8..=9_u8)` not covered
 --> src/main.rs:5:26
  |
5 |     let (b1, b2) = match (n, m) {
  |                          ^^^^^^ pattern `(129_u8..=u8::MAX, 0_u8..=9_u8)` not covered
  |
  = note: the matched value is of type `(u8, u8)`

この網羅性チェッカーはかなり優秀なので、 match 式は背中を預けるような心持ちで使えるというわけです!

if ガード

もう一つ、 match 式に固有の機能として ifガード があります。こちらは、パターンの後に if 式に似た何かを記載できる機能です。

他言語に寄せて普通にfizzbuzzを書く例がわかりやすいでしょう。

ifガード版fizzbuzz
fn fizz_buzz(n: usize) -> FizzBuzzValue {
    match n {
        n if n % 15 == 0 => FizzBuzz,
        n if n % 3 == 0 => Fizz,
        n if n % 5 == 0 => Buzz,
        _ => Other(n)
    }
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=052093cabf96c9925ae839ba049e0b7e

if else if else ... を避けられるほか、「他言語の switch 文だと書ける4のにRustの match 式だと書けない(書きにくい)!」みたいなこともこのifガードのおかげで起きないんじゃないかなと思います。

一つ注意点としては、当たり前といえばそうですが、ifガードの中身までは網羅性の確認が行き届かない点です。

以下の書き方では、網羅的なはずですがコンパイルエラーになります。網羅性の担保にはそのほかの論駁不可能パターン等が必要になるでしょう。

ifガード版fizzbuzz2 (コンパイルエラー)
fn fizz_buzz(n: usize) -> FizzBuzzValue {
    match n {
        n if n % 15 == 0 => FizzBuzz,
        n if n % 3 == 0 => Fizz,
        n if n % 5 == 0 => Buzz,
        n if n % 3 != 0 && n % 5 != 0 => Other(n),
    }
}
エラー内容。0は一番上に該当するはずだがカバーされていないとなる
error[E0004]: non-exhaustive patterns: `0_usize..` not covered
  --> src/main.rs:12:11
   |
12 |     match n {
   |           ^ pattern `0_usize..` not covered
   |
   = note: the matched value is of type `usize`
   = note: match arms with guards don't count towards exhaustivity
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
   |
16 ~         n if n % 3 != 0 && n % 5 != 0 => Other(n),
17 ~         0_usize.. => todo!(),
   |

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=2c6acb028f00c471be0c51903325db30

if let

if let式
if let 論駁可能パターン =  { /* パターンに一致時実行 */ }

// または

if let 論駁可能パターン =  { /* パターンに一致時実行 */ } else { /* パターンに合致しない時実行 */ }

Rust 1.88 で機能追加された主役、 if let は冒頭のソースコードから顔を出してきました!

Let chains (再掲)
fn jsons_json_in_json_article_checker(contents: &str) -> anyhow::Result<bool> {
    // if let 式
    if let Contents {
        author,
        content: Content::Other { body: json_str },
        ..
    } = serde_json::from_str(contents)?
    // 1.88からは、さらに条件をくっつけられる!
    && author == "Json"
    // 1.88からは、let もくっつけられる!
    && let JsonInJsonContent::Article { body } = serde_json::from_str(&json_str)? {
        println!("It's Json's Article: {}", body);
        Ok(true)
    } else {
        Ok(false)
    }
}
全体
Rust
#![allow(unused)]

use serde::Deserialize;

#[derive(Deserialize)]
struct Contents {
    id: usize,
    author: String,
    content: Content,
}

#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum Content {
    Article {
        title: String,
        body: String,
    },
    Other {
        body: String,
    },
}

#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum JsonInJsonContent {
    Article {
        body: String,
    },
    Other {
        body: String,
    },
}

fn jsons_json_in_json_article_checker(contents: &str) -> anyhow::Result<bool> {
    if let Contents {
        author,
        content: Content::Other { body: json_str },
        ..
    } = serde_json::from_str(contents)?
    && author == "Json"
    && let JsonInJsonContent::Article { body } = serde_json::from_str(&json_str)? {
        println!("It's Json's Article: {}", body);
        Ok(true)
    } else {
        Ok(false)
    }
}

fn main() -> anyhow::Result<()> {
    let contents = r#"{
    "id": 13,
    "author": "Json",
    "content": {
        "type": "other",
        "body": "{ \"type\": \"article\", \"body\": \"Today is Friday.\" }"
    }
}"#;

    let true = jsons_json_in_json_article_checker(contents)? else {
        println!("Pattern matching failed...");
        return Ok(());
    };
    
    Ok(())
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=9ff3d4246ae2aeea4ac956ebd3409734

if let 式それ自体は「論駁可能パターンに合致していたら真の節を実行し、(もし枝があれば)合致していないときは else 節を実行する」という、ある意味で論駁可能パターンによるパターンマッチを最もよく説明してくれる構文です。特殊ながら扱いやすい感がありますね!

よく if let Some(n) = ... {} という形で Option 型の値で分岐する例ばかり見ますが、ここまでの解説からわかる通り論駁可能パターンならば何でも大丈夫です!

普通こうは書かないかも...
if let 4 = n {
    println!("ミスタ「4はやめてくれ...」");
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=e6f9c92366df17d40db4b20a1512a028

ちなみに論駁不可能パターンも指定すること自体はできるのですが、警告が出ます。

if let式に論駁不可能パターンを渡すと警告
if let m = n {
    println!("{m}");
}
warning: irrefutable `if let` pattern
 --> src/main.rs:4:8
  |
4 |     if let m = n {
  |        ^^^^^^^^^
  |
  = note: this pattern will always match, so the `if let` is useless
  = help: consider replacing the `if let` with a `let`
  = note: `#[warn(irrefutable_let_patterns)]` on by default

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=417a0b4a2b16bd67e71b25975e99e93f

Let chains

Rust 1.88 記念で書いた記事なので一応もちろん(?)Let chainsに言及しておきます。

パターンや真偽値を && で繋げられるようになったのがLet chainsです!

Some でかつ偶数の時に処理 (従来)
if let Some(m) = n {
    if m % 2 == 0 {
        println!("{m} が存在していて偶数だった");
    }
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=2510c3733cee36de462c9cf5aeade675

今までは、 if let 式でマッチした後にさらにその中身で条件分岐する場合、上記の通り if をネストするしかありませんでした。

1.88 からはLet Chainsが追加されたので、これを一つの if let 式の中で書けるようになりました!

1.88からのLet Chainsを利用
if let Some(m) = n
    && m % 2 == 0
{
    println!("{m} が存在していて偶数だった");
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=91aa03d47c87e11558b6d772600e22a9

|| (or) は使えないことに注意

Let chains は見た目の上でこそ条件式を && でつなぐように書きますが、真偽値を結ぶ && と全く同じかと聞かれるとそうではないので、「同じ if の条件節部分で連続してパターンマッチできる」以上のことは期待しないほうがよさそうでしょう。

そのことを一番実感できる例として、 if let 式では || (or) を使ったパターンマッチの連結はできないことが挙げられます。

letをorでつなぎたい (コンパイルエラー)
enum Mode {
    Mode1(usize),
    Mode2(usize),
    Other
}

use Mode::*;

if let Mode1(n) = m || let Mode2(n) = m {
    println!("Mode1 or Mode2 called with {}.", n);
}
エラー内容
error: `||` operators are not supported in let chain conditions
  --> src/main.rs:12:25
   |
12 |     if let Mode1(n) = m || let Mode2(n) = m {
   |                         ^^

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=32d4ece4f6e1d3747270fbe533c6271b

そもそも論理値演算とは関係なく、パターン同士は | で連結できるのでした

パターンは | でつなぐ
enum Mode {
    Mode1(usize),
    Mode2(usize),
    Other
}

use Mode::*;

if let Mode1(n) | Mode2(n) = m {
    println!("Mode1 or Mode2 called with {}.", n);
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=f46fee6b6937044f0931510fe0f192c7

Let chains の && は見た目的にわかりやすいから使われているのであって、論理値演算ではないことを覚えておきましょう。

while let

while let文
while let 論駁可能パターン =  {...}

筆者が地味に好きな構文です。「パターンにマッチする間だけ」繰り返したい時に使えます。

次はmpscでの活用例です。 rx.recv()Ok の間、すなわちチャネル間の通信が有効でまだ有効な値が rx にやってくる間、処理されます。

mpscでのwhile let活用例
use std::sync::mpsc::channel;

fn main() {
    let (tx, rx) = channel();
    
    std::thread::spawn(move || {
        for n in 0..10 {
            tx.send(n).unwrap();
        }
    });
    
    // 値を取り出せている間だけ繰り返し
    while let Ok(n) = rx.recv() {
        println!("{n}");
    }
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=b41d9e269c428fb7253c63e52b476051

ただ実は std::sync::mpsc::channel::ReceiverIntoIterator を実装しているので、わざわざ while let を利用しなくても for 文で同じ処理を書けちゃったりします :sweat_smile:

rxをイテレータとして扱う例
use std::sync::mpsc::channel;

fn main() {
    let (tx, rx) = channel();
    
    std::thread::spawn(move || {
        for n in 0..10 {
            tx.send(n).unwrap();
        }
    });
    
    // 値を取り出せている間だけ繰り返し
    for n in rx {
        println!("{n}");
    }
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=d1129d2d995ffc5d2f9c44f1e88f8f25

tokio::sync::mpsc::Receiver あたりは IntoIterator を実装していないので、こちらを利用する場合は while let が引き続き効果的かもしれません。

while let が輝く例: ダイクストラ法

while let を使うことで、「tokio::sync::mpsc::Receiver で有効値が返ってくる間だけループ」みたいなことができると紹介しました。

それ以外にも、値の取り出し元がループ内で変化する場合などはもっと while let が輝くシーンだったりします。

その代表例にダイクストラ法があります。アルゴリズムの詳細はここでは省略しますが、次のようなソースコードになります!

ダイクストラ法
fn dijkstra(graph: &Graph, start: Node) -> HashMap<Node, Cost> {
    let mut resolved: HashMap<Node, Cost> = HashMap::new();
    let mut priority_queue: BinaryHeap<Move> = BinaryHeap::new();

    // スタート地点を追加
    priority_queue.push(Move {
        to: start,
        total_cost: 0,
    });

    // ヒープ木から最小コストで到達可能なノードを探索
    while let Some(Move {
        to: current_node,
        total_cost,
    }) = priority_queue.pop()
    {
        // 既に到達済みならスキップ
        if resolved.contains_key(&current_node) {
            continue;
        }

        // 最短距離で到達したノードを記録
        resolved.insert(current_node, total_cost);

        // 全ノードに到達していたら終了
        if resolved.len() >= graph.len() {
            break;
        }

        // 隣接する頂点への移動候補を追加
        if let Some(edges) = graph.get(&current_node) {
            for &Edge { to, cost } in edges {
                if !resolved.contains_key(&to) {
                    priority_queue.push(Move {
                        to,
                        total_cost: total_cost + cost,
                    });
                }
            }
        }
    }

    resolved
}
全体
ダイクストラ法
use proconio::input;
use std::cmp::Ord;
use std::collections::BinaryHeap;
use std::collections::HashMap;

type Node = usize;
type Cost = usize;

#[derive(Clone, Copy, Debug)]
struct Edge {
    to: Node,
    cost: Cost,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct Move {
    to: Node,
    total_cost: Cost,
}

impl Ord for Move {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.total_cost.cmp(&other.total_cost).reverse()
    }
}

impl PartialOrd for Move {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

type Graph = HashMap<Node, Vec<Edge>>;

fn dijkstra(graph: &Graph, start: Node) -> HashMap<Node, Cost> {
    let mut resolved: HashMap<Node, Cost> = HashMap::new();
    let mut priority_queue: BinaryHeap<Move> = BinaryHeap::new();

    // スタート地点を追加
    priority_queue.push(Move {
        to: start,
        total_cost: 0,
    });

    // ヒープ木から最小コストで到達可能なノードを探索
    while let Some(Move {
        to: current_node,
        total_cost,
    }) = priority_queue.pop()
    {
        // 既に到達済みならスキップ
        if resolved.contains_key(&current_node) {
            continue;
        }

        // 最短距離で到達したノードを記録
        resolved.insert(current_node, total_cost);

        // 全ノードに到達していたら終了
        if resolved.len() >= graph.len() {
            break;
        }

        // 隣接する頂点への移動候補を追加
        if let Some(edges) = graph.get(&current_node) {
            for &Edge { to, cost } in edges {
                if !resolved.contains_key(&to) {
                    priority_queue.push(Move {
                        to,
                        total_cost: total_cost + cost,
                    });
                }
            }
        }
    }

    resolved
}

// 問題: https://atcoder.jp/contests/abc214/tasks/abc214_c
fn main() {
    input! {
        n: usize,
        ss: [usize; n],
        ts: [usize; n],
    }

    let mut graph: Graph = [(
        0,
        ts.into_iter()
            .enumerate()
            .map(|(i, t)| Edge { to: i + 1, cost: t })
            .collect(),
    )]
    .into_iter()
    .collect();

    for (i, s) in ss.into_iter().enumerate() {
        let from = i + 1;
        let to = if i + 2 <= n { i + 2 } else { 1 };

        graph.insert(from, vec![Edge { to, cost: s }]);
    }

    let reached = dijkstra(&graph, 0);

    for i in 1..=n {
        let cost = reached.get(&i).unwrap();
        println!("{cost}");
    }
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=62f9ee7a31f61b98d929349c1ab18dde

ダイクストラ法では、「優先度付きキュー (ヒープ木) から移動候補が取り出せるうちはループ」という処理が必要です。一方で、優先度付きキューにはループ内にて動的に移動候補が追加されていきます。「 IntoIterator 等でイテレータにして for で回す」みたいに単純には書けないため、ダイクストラ法みたいなアルゴリズムを記述する際は while let が活躍するわけです!

while let でダイクストラ法を記述するのが好きなため、本記事に例として載せさせていただきました!

let else

let else文
let 論駁可能パターン =  else {
    never型を返す処理
};

構文として最後に紹介するのは let else 文です! let else 文も比較的新しい構文で、1.65にて追加されました。

論駁可能パターンに合致する時はパターン内の変数に値を束縛します。もし合致しない場合は、束縛を諦め else 節を実行します。 else 節はnever型( ! )を返す(最後に発散する)処理のみしか書けないため、論駁可能パターンの束縛が失敗した際も矛盾なく記述できます。

let else文の例
for i in 1..=15 {
    let v = fizz_buzz(i);

    let FizzBuzzValue::Other(j) = v else {
        continue; // never 型の処理
    };

    println!("{j}");
}

// ↑ を if let 式で書くと ↓

for i in 1..=15 {
    let v = fizz_buzz(i);

    let j = if let FizzBuzzValue::Other(k) = v {
        k
    } else {
        continue; // never 型の処理
    };

    println!("{j}");
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=3f291a754bb65671d4386cd59d1fbadf

本記事のここまでを踏まえて一言で言えば、「論駁可能パターン用の let」というポジションなので、ある意味で let 文や if let 並にプリミティブな構文と言えるかもしれません。

ところが、 if let を利用する機会が多い Option 型や Result 型には let else を利用するより便利なメソッドが用意されていたり、 else 節には returncontinue, panic!(...) といった発散する(never型の)処理しか書けないという使い勝手の悪さが影響して、なかなか利用機会が少ない構文だったりします。

マクロを書くために syn クレートを使う際などは結構お世話になるのですがね...ベストな使いどころを見つけたらスマートに使ってみたいというロマン構文です :sparkles:

以前書いた記事の方が詳しいのでそちらもぜひご一読いただけると幸いです!

Rustのlet-else文気持ち良すぎだろ #Rust - Qiita

matches! マクロ

最後におまけで、パターンマッチを利用できるマクロ matches! を紹介します!

matches! マクロ
matches!(, パターン)

このマクロは次に展開されるシンプルなものです↓

match  {
    パターン => true,
    _ => false
}

「パターンマッチに合致するかどうか」だけを返すメソッド等を定義する際に、可読性を向上させてくれるでしょう!

matches! マクロ利用例
#[derive(Debug)]
enum FizzBuzzValue {
    FizzBuzz,
    Fizz,
    Buzz,
    Other(usize)
}

impl FizzBuzzValue {
    fn is_not_other(&self) -> bool {
        !matches!(self, Self::Other(_))
    }
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=21bf9704c26f01c6e12ffc9bbe944de3

まとめ

パターンマッチの諸々を紹介してきました! let () = {};let (val, _): (usize, usize); みたいな普段絶対書かないような変な例をいろいろ出してきましたが、むしろこの変な例のおかげでRustのパターンマッチチャンス:sparkles::sparkles:は至る所に潜んでいることを示せたのではないでしょうか...?

本記事を通してパターンマッチを強い味方に付けていただけたならば幸いです! :muscle:

ここまで読んでいただきありがとうございました! :bow::bow:

  1. proc_macro::Span::line 等、マクロで使用する Span にあるメソッドの安定化が個人的には一番アツかったですが、それはまた別な話...いつか話せればと思います

  2. Rustの組み込み型のうち、 Copy トレイトが付与されている型(スタックに保存できる型)という認識で大体あっています。

  3. match 式のデフォルトパターンは論駁不可能パターンですし、論駁不可能パターンが一つだけある match 式も書けます。ただ後者に関しては let 文で良いかなと思います。

  4. 他言語の switch 文のポテンシャルを失念したので、すぐにはよい例が思いつかないですが...

6
2
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
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?