先月、 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
式の周辺知識、という位置付けでパターンマッチのテクニック集を紹介したいと思います!
基礎事項についてはほぼ公式ドキュメントのまとめ直しです。
一次ソースに当たりたい方は以下も見てみてください!
TODO: TL;DR的なものの記載
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,
}
}
}
プリミティブ型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}"),
}
もっと面白い例もあるのですがネタが尽きてしまうので記事の後半で...
この先 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}は奇数"),
}
}
ここで示した 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);
}
}
論駁不可能パターン
どんな場面でも絶対に受け入れ・束縛に成功する パターンマッチを論駁可能なパターンと言います。
通常の変数束縛はある意味で絶対成功するパターンマッチです。その他にも、構造体の分解なども必ず成功するため論駁不可能パターンとして利用可能です。
通常の 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
式のアームでよく使える記法です!
match c {
'a' | 'A' => println!("a か A"), // 'a' と 'A' のパターンを結合
c => println!("{c}"),
}
ところで、 let
文は論駁不可能なパターンしか用いることができないのでした。しかし、 論駁不可能なら文句なし なわけです。というわけで、実は次のソースコードは問題ありません!
fn hoge(opt: Option<usize>, default: usize) {
let ((Some(n), _) | (_, n)) = (opt, 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}"),
}
ちなみに全パターンを網羅(exhaustive)する必要がある match
式のアームに使った場合、この記法も使ってもしっかりと網羅性チェックは行われるので安心です
使用頻度は高くないと思いますが、知っておいて損はないでしょう。
テクニック4
@
で値を束縛しつつパターン検証
「ある値があるパターンにマッチするかを調べたい」、でも、「元の値を利用したい」、そんな時に利用できるのが @
です!
let age = 15;
match age {
..13 => println!("子供"),
n @ 13..=19 => println!("{n} 歳はティーンエイジャー"),
n => println!("{n} 歳は大人"),
}
上記では範囲指定でのみ使っていますが、列挙型を深いネストと共にパターンマッチさせる場合などにも便利だったりします
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 Point { x, y } = p;
良い機会なので「ちなみに」として書きますが、 let
には変数宣言だけを行う機能もあります。
fn func(flag: bool) -> usize {
let (val, _): (usize, usize);
// val には一度だけ代入可能
if flag {
val = 10;
} else {
val = 20;
}
val * val
}
Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=01f7e2539251d24b1d8b664e100240fb
例で書いた様に、パターンを利用して変数宣言を書くと非常にシュールですね...
関数・クロージャの引数
先に挙げた例の通り、関数やクロージャの引数部分でパターンマッチを行うことができます!
#[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: usize,
// 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 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
式
fn fizz_buzz(n: usize) -> String {
match (n % 3, n % 5) {
(0, 0) => "fizzbuzz".to_string(),
(0, _) => "fizz".to_string(),
(_, 0) => "buzz".to_string(),
_ => n.to_string()
}
}
網羅性 (exhaustive)
if ガード
if let
式
Rust 1.88 の主役、 if let
は冒頭のソースコードから顔を出してきました!
while let
文
let else
文
以前書いた記事の方が詳しいのでそちらもぜひご一読いただけると幸いです!
never
型
matches!
マクロ
まとめ
-
proc_macro::Span::line 等、マクロで使用する
Span
にあるメソッドの安定化が個人的には一番アツかったですが、それはまた別な話...いつか話せればと思います ↩ -
Rustの組み込み型のうち、
Copy
トレイトが付与されている型(スタックに保存できる型)という認識で大体あっています。 ↩ -
match
式のデフォルトパターンは論駁不可能パターンですし、論駁不可能パターンが一つだけあるmatch
式も書けます。ただ後者に関してはlet
文で良いかなと思います。 ↩