次の書き方に違和感を持った方はぜひ本記事を読んでみてほしいです!
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
読み終わる頃には理解できるはず!(多分 )
先月、 Rust 1.88 がリリースされました
特に「 if let
式で let
を &&
で繋げられる」 Let chains というのが目玉機能として挙げられています1!
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)
}
}
全体
#![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
という変数に束縛され、以降同スコープ内で変数 x
と y
が使えるようになっています。
分解はタプルの他にも構造体・列挙体・配列など色々な構文要素で可能です!
#[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
による定数宣言
合致判定
最後の役割が名前通りパターンにマッチするかどうかの検証です。ただし、この役割は後述の「論駁可能パターン」のみが持ちます。
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つに分類されます!
- 論駁可能パターン ( refutable pattern )
- 論駁不可能パターン ( 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::Apple
や PurchaseResult { 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 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
_
や ..
で値を無視する
分解構文的にパターンマッチを見た際、利用しないフィールドがある時もあるでしょう。 _
や ..
はそんな時に利用できたりします!
#[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
の値は使用しないため _
としています。
..
を使えば、「以降のフィールドを無視」といったことも可能です。たくさんフィールドがある際などに便利です。
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
式のアームでよく使える記法です!
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
型(文字型のこと)のみこの記法を用いたパターンを書くことが可能です!
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
式のアームに使った場合、この記法も使ってもしっかりと網羅性チェックは行われるので安心です
使用頻度は高くないと思いますが、知っておいて損はないでしょう。
テクニック4
@
で値を束縛しつつパターン検証
「ある値があるパターンにマッチするかを調べたい」、でも、「元の値を利用したい」、そんな時に利用できるのが @
です!
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
上記では範囲指定でのみ使っていますが、列挙型を深いネストと共にパターンマッチさせる場合などにも便利だったりします。
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
トレイト付与型を基本に捉えておいた方がよい)
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を書いている分には必要としない機能かもしれません。
&
の方はクロージャの引数でしばしば目にすることがあります。
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 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 論駁不可能パターン 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
式を用いたRust版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を書く例がわかりやすいでしょう。
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ガードの中身までは網羅性の確認が行き届かない点です。
以下の書き方では、網羅的なはずですがコンパイルエラーになります。網羅性の担保にはそのほかの論駁不可能パターン等が必要になるでしょう。
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),
}
}
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 論駁可能パターン = 式 { /* パターンに一致時実行 */ } else { /* パターンに合致しない時実行 */ }
Rust 1.88 で機能追加された主役、 if let
は冒頭のソースコードから顔を出してきました!
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)
}
}
全体
#![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 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です!
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
式の中で書けるようになりました!
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) を使ったパターンマッチの連結はできないことが挙げられます。
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 論駁可能パターン = 式 {...}
筆者が地味に好きな構文です。「パターンにマッチする間だけ」繰り返したい時に使えます。
次はmpscでの活用例です。 rx.recv()
が Ok
の間、すなわちチャネル間の通信が有効でまだ有効な値が 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();
}
});
// 値を取り出せている間だけ繰り返し
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::Receiver
は IntoIterator
を実装しているので、わざわざ while let
を利用しなくても for
文で同じ処理を書けちゃったりします
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(¤t_node) {
continue;
}
// 最短距離で到達したノードを記録
resolved.insert(current_node, total_cost);
// 全ノードに到達していたら終了
if resolved.len() >= graph.len() {
break;
}
// 隣接する頂点への移動候補を追加
if let Some(edges) = graph.get(¤t_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(¤t_node) {
continue;
}
// 最短距離で到達したノードを記録
resolved.insert(current_node, total_cost);
// 全ノードに到達していたら終了
if resolved.len() >= graph.len() {
break;
}
// 隣接する頂点への移動候補を追加
if let Some(edges) = graph.get(¤t_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 {
never型を返す処理
};
構文として最後に紹介するのは let else
文です! let else
文も比較的新しい構文で、1.65にて追加されました。
論駁可能パターンに合致する時はパターン内の変数に値を束縛します。もし合致しない場合は、束縛を諦め else
節を実行します。 else
節はnever型( !
)を返す(最後に発散する)処理のみしか書けないため、論駁可能パターンの束縛が失敗した際も矛盾なく記述できます。
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
節には return
や continue
, panic!(...)
といった発散する(never型の)処理しか書けないという使い勝手の悪さが影響して、なかなか利用機会が少ない構文だったりします。
マクロを書くために syn
クレートを使う際などは結構お世話になるのですがね...ベストな使いどころを見つけたらスマートに使ってみたいというロマン構文です
以前書いた記事の方が詳しいのでそちらもぜひご一読いただけると幸いです!
matches!
マクロ
最後におまけで、パターンマッチを利用できるマクロ matches!
を紹介します!
matches!(式, パターン)
このマクロは次に展開されるシンプルなものです↓
match 式 {
パターン => true,
_ => false
}
「パターンマッチに合致するかどうか」だけを返すメソッド等を定義する際に、可読性を向上させてくれるでしょう!
#[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のパターンマッチチャンスは至る所に潜んでいることを示せたのではないでしょうか...?
本記事を通してパターンマッチを強い味方に付けていただけたならば幸いです!
ここまで読んでいただきありがとうございました!
-
proc_macro::Span::line 等、マクロで使用する
Span
にあるメソッドの安定化が個人的には一番アツかったですが、それはまた別な話...いつか話せればと思います ↩ -
Rustの組み込み型のうち、
Copy
トレイトが付与されている型(スタックに保存できる型)という認識で大体あっています。 ↩ -
match
式のデフォルトパターンは論駁不可能パターンですし、論駁不可能パターンが一つだけあるmatch
式も書けます。ただ後者に関してはlet
文で良いかなと思います。 ↩ -
他言語の
switch
文のポテンシャルを失念したので、すぐにはよい例が思いつかないですが... ↩