🎨

Rustのパターンマッチってアートだヨネ

に公開

こんにちワ! Rust大好きなサーバーサイドエンジニアのnamniumと申します。

Rustのソースコードはしばしばアートのように振る舞います。その中でもRustのパターンマッチは言語を代表する機能だけあって、綺麗に書けた時の美しさといえば筆舌に尽くしがたいです! 今回はそんなRustパターンマッチの世界を皆様に紹介したく無理やり筆を執りました!

次の"アート"に違和感を持った方はぜひ本記事を読んでみてほしいです[1]

  • 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=1f772f6fb10721a1016343b0132136ea

読み終わる頃には美しく感じるはず!(多分💦)

今年2025年の6月、 Rust 1.88 がリリースされました🥳🥳(ちなみに執筆時である2025年10月現在の最新は 1.90.0 です😅)

https://blog.rust-lang.org/2025/06/26/Rust-1.88.0/

https://aznhe21.hatenablog.com/entry/2025/06/27/rust-1.88

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

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パターンマッチの世界を見ていきましょう!

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' | '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=5a2638bfd295edc88993ced4f8b7431d

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

この先 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で 「変数の束縛(あるいは変数宣言)ができるならその箇所はパターンマッチである」 という事実です。

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

パターンマッチチャンス特集
#[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 {...} の例を見るとわかる通り、論駁可能かどうかはパターンの複雑さには特に関係なく、純粋に 「常に受け入れ・束縛可能かどうか」 のみで決まります。

別な例
#[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

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

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

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

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

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: 実際に束縛は行われる、すなわち所有権が奪われたりする
  • _: 束縛すら行われない。所有権も奪われない

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

✨テクニック2✨ | でパターン連結

| を使うことで複数パターンを結合することができます。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 )が同じだけ用意されること

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

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

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

Rust
let n: u8 = 10;

match n {
    ..=127 => println!("7F 以下"),
    128.. => println!("80 以上"),
}

let chr = 'Z';

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

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

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

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

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

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

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

✨テクニック5✨ 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

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

関数・クロージャの引数

関数・クロージャ
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 文にしろインデックスと値についてタプルでの受け取りが発生してしばしば書くんじゃないかと思います!

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

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

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 の let chains 機能追加に関連して、 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に言及しておきます。

https://rust-lang.github.io/rfcs/2497-if-let-chains.html

パターンや真偽値を && で繋げられるようになったのが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 文で同じ処理を書けちゃったりします😅

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 クレートを使う際などは結構お世話になるのですがね……ベストな使いどころを見つけたらスマートに使ってみたいというロマン構文です✨

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のパターンマッチチャンス✨✨は至る所に潜んでいることを示せたのではないでしょうか……?

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

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

(このまとめはQiita掲載時のまとめです。最後に改めて所感を入れています。もうちょっとだけ続くんじゃ)

おまけ: Rust 1.88とhooqクレート

ここからはQiita掲載時には載せなかったおまけという名の宣伝を載せたいと思います!

筆者的には、実はRust 1.88にアップデートされて一番嬉しかったのはLet chainsではなくproc_macro::Span::line メソッド等の安定化でした! というお話です。

hooqクレート

業務でのRust利用を通じ、Rustは ? 位置でのトレースを得られにくいことに気がついた筆者は、 ? 位置に自動的に Result 型(や Option 型)のメソッドを挿入してくれるマクロを作ろうと思い立ちました。

そこで半年ほどかけて作成したのがhooqクレートです!

https://github.com/anotherhollow1125/hooq

どの行でエラーが発生したかはエラー解析において重要な情報でしょう。どの位置にある ? でエラーが発生したかは、anyhowクレートの with_context メソッド [5]line!() マクロを利用したり、 Backtrace をエラーに含めるなどをすることで取得できます。

以下例です。なお、 .get(...)Option 型を返すメソッドですが with_context のおかげで Result 型を返すようになっています。

with_contextを各?に
use anyhow::Context;
use anyhow::Result;

fn display_description(val: &toml::Value) -> Result<()> {
    let description = val
        .get("package")
        .with_context(|| format!("package not found. (L{})", line!()))?
        .get("description")
        .with_context(|| format!("description not found. (L{})", line!()))?
        .as_str()
        .with_context(|| format!(".as_str() return None. (L{})", line!()))?;

    println!("description: {description}");

    Ok(())
}

fn main() -> Result<()> {
    let cargo_toml = toml::from_str(&std::fs::read_to_string("Cargo.toml")?)?;

    display_description(&cargo_toml)?;

    Ok(())
}

description を記述せずに実行すると次に示すエラーが出ます。 line!() マクロのおかげで L9 という表記がなされ、その上の .get("description")None を返したのだとわかります。( with_context のおかげで Option 型は Result 型に変換されています。)

descriptionなしで実行してみると
$ cargo run -q
Error: description not found. (L9)

しかし本筋とは無縁なのに毎回 .with_context(|| format!(".as_str() return None. (L{})", line!()))? などと書くのははっきり言って苦痛です。そこで、hooqの出番です! 属性風マクロでこの記述を省略できます!

hooqで書き直すと
use anyhow::Context;
use anyhow::Result;
use hooq::hooq;

#[hooq(anyhow)]
fn display_description(val: &toml::Value) -> Result<()> {
    let description = val.get("package")?.get("description")?.as_str()?;

    println!("description: {description}");

    Ok(())
}

#[hooq(anyhow)]
fn main() -> Result<()> {
    let cargo_toml = toml::from_str(&std::fs::read_to_string("Cargo.toml")?)?;

    display_description(&cargo_toml)?;

    Ok(())
}

ソースコード上に with_context はありませんが、 hooq が挿入してくれるため with_context の恩恵を受けることができます!

descriptionなしで実行してみると
$ cargo run -q
Error: [main.rs:L18] ...iption(& cargo_toml)

Caused by:
    [main.rs:L7] ... .get("description")

どの ? でエラーが起きたかは相変わらず詳細に得られる上に、プログラムの本筋はとても読みやすくなったと思います!

1.88 proc_macro::Span::line 安定化のおかげでstableに

hooq実装中にあった大きな課題に、「フック対象の ? が存在する行の取得が難しい」というものがありました。

with_context を利用した例では、 line!() マクロを利用していましたが、なんと line!() マクロは属性風マクロ内で呼ぶとマクロ付与位置を指し続ける [6] という性質があり、今回の目的では全く使い物になりません!

属性風マクロにおいて付与対象構文の行数を得るには、どうしても proc_macro::Span::line の情報にアクセスする必要がありました。

hooqの開発を始めた当初はこのメソッドはnightlyでないと利用できず、コンパイラの都合上nightlyビルドは利用者側のクレートでも必要になるという辛い現状がありました……。

が、🥳🥳 1.88でこのAPIが安定化しました 🥳🥳!

おかげさまで(MSRVは1.88になってしまいましたが)hooqは全ての機能をstableで供給できるようになりました。Let chainsが霞むレベルで嬉しかったです!

……そんなわけで、気になった方はぜひhooqを使ってみてください! 以上、hooqの宣伝記事でした! (オイ

まとめ2・所感

hooq分の加筆修正が入り締まらない感じになってしまったので、全体を通して改めてのまとめです!

Rustを使うメリットを挙げると枚挙に暇がありませんが、パターンマッチはメリットであるだけに留まらず、Rustでプログラミングを行う際の根幹の一つと言えます。Rustのよく言われるメリットに「表現力が強い線形型システム」がありますが、パターンマッチはまさしくこの線形型システムによる恩恵を処理フロー上で受けるための機能ですから、根幹を担う機能であるに決まっています。

線形型システムとパターンマッチは、なんなら半年かけてまでhooqを作ろうと決心した理由の一つですらあります。エラー発生行を楽にトレースする手段としては、 std::backtrace::Backtrace 構造体を自分が使いたいエラーに入れるというものが既にありました。しかしこの方法を取ると処理に関係がない余計なフィールドをエラー型に含める必要が出てきて、好ましい体験とは言えません。

hooqならば、 with_context などのメソッドを呼び出すオーバーヘッドが挟まってしまいますが、エラー型の表現をシンプルなままに留め、 match 式なども相変わらずスッキリさせたままオブザーバビリティを得ることができます。 match 式の書き味、つまりパターンマッチがhooq誕生のキッカケに関わっているのです!

今回の記事でそんなパターンマッチをアートのように表現してまとめられたことは個人的にはとても良い経験でした。改めて、ここまで読んでいただきありがとうございました!🙇🙇

脚注
  1. 本記事のタイトルはマギアレコードに登場する某キャラのセリフ風に……まどドラハーフアニバーサリーおめでとうございます! ↩︎

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

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

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

  5. anyhow::Context トレイトには context というメソッドも定義されているのですが、こちらの引数はエラーでない時も毎回評価されてしまうため、望まない挙動をすることが多いです。与えられたクロージャは遅延評価される with_context でも同様のことが可能なため筆者は with_context の方を多用しています。 ↩︎

  6. 例で言うと、一つ目の #[hooq(anyhow)] は5行目なので line!() は常に 5 を返します。 ↩︎

Discussion