先日、Rust バージョン1.65.0が利用できるようになりました
その中でも個人的に最も嬉しい機能追加がlet-else
文になります!
let-else
文の見た目
let-else
文はこんな見た目をしています。
// let 論駁可能パターン = 値 else { never型を返す処理 };
let Ok(val) = reqwest::get(url).await else { return };
このコードの意味としてはreqwest::get(url).await
がOk(結果)
を返してきたらval
に束縛し、ダメだったら関数を抜ける、になります。
if-let
式
let-else
文の詳細を説明する前に、まずはRustのif-let
式について説明いたします。
Rustは式指向言語のためif
も標準で式になっています。よく他言語では三項演算子使用で宗教戦争が起きていますが「if
"式"があれば争いなんて起きないのに...(トオイメ」といつも思っています。
fn main() {
let arg = std::env::args().nth(1).unwrap_or("bar".to_string());
let val = if &arg == "bar" {
"hoge".to_string()
} else {
"fuga".to_string()
};
println!("{}", val);
}
それぞれのブロックの最後の式が返す値として評価され、val
に束縛されます。三項演算子のように使う場合は、真のときに返す型と偽のときに返す型が一致している必要があります。ただし、!
(never)型を除きます。(後述)
そしてRust独特なif
式にif-let
式というものがあります。Rustのlet
にはパターンマッチの機能があり、パターンに合う時を真とするのがif-let
式になります。以下に示す例1では、parse
メソッドが返すResult
型変数がOk(i32型)
である時にn
にパース結果を束縛してそれを返り値とし、val
に束縛しています。合致しない場合はreturn
しています。
例1: Result
型
fn main() {
let arg = std::env::args().nth(1).unwrap_or("NaN".to_string());
let val = if let Ok(n) = arg.parse::<i32>() {
n
} else {
println!("Caution!: {} is not a number, please pass a number.", arg);
return;
};
println!("{}^2 = {}", val, val * val);
}
もう少し正確に説明すると、普通のlet
文は論駁不可能パターン(必ず成功する束縛)のみ取り、if-let
式は論駁可能パターンも束縛可能とする機能です。簡単にいえば、let
にはもともと構造体やタプルを展開して束縛する機能、いわゆる分割代入があり、列挙体等複数可能性があるパターン(これを論駁可能なパターンといいます)についてはif-let
式で受け取れるようにした、という感じでしょうか。そしてmatch
式は論駁可能パターンに対して複数のパターンマッチを行い分岐するif-let
式の強化版になっています。
例2: 構造体の分割代入
#[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);
// 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);
}
}
上記例ではif-let
式の返り値を使わないためelse
節を省略できます。
Result
型やOption
型で使われることが多いif-let
ですが、先に出てきた構造体のように、それ以外の論駁可能パターンにも使えます。
例3: 任意の列挙型
enum MyState {
Samumi,
Nemumi,
Other(String),
}
fn main() {
let my_state = MyState::Other("Hello".to_string());
let state_inner = if let MyState::Other(state_inner) = my_state {
state_inner
} else {
println!("Your state is Samumi or Nemumi.");
return;
};
println!("Your state is Other({})", state_inner);
}
ただしMyState
のように3項目以上ある列挙体では見通しが良いmatch
式を使うべきでしょう。
let state_inner = match my_state {
MyState::Other(state_inner) => state_inner,
MyState::Samumi | MyState::Nemumi => { // `_ => {` でも可
println!("Your state is Samumi or Nemumi.");
return;
}
};
never型 (if
のelse
節の返り値について)
ついでにここで!
(never)型の説明もしておきます。(使うので)
通常、if
やif-let
を"式"として使う場合は、真の時の値と偽の時の値の型は一致する必要がありました。
しかし、他言語でも普通に考えられる「if
文中にreturn
やcontinue
、あるいはthrow Error
(panic!
を指すとします)する」ケースではどうすれば良いでしょう?例1や例3がまさしくこのケースになっていますね。
実はこれらの処理構文はRustにおいては!
(never)型を返すものとして扱われ、never型はif
式やmatch
式の型推論においてはその他の枝の型に型強制される特殊な型になっています。
return
やcontinue
は!
(never)型を返す(ものと仮定されている)ので、その上で型推論が行われコンパイルエラーにはならないのです。こじつけ感が強い
ではlet-else
文とは?
前置きが長くなってしまいましたがようやくメインディッシュです。
ずばり、「論駁可能パターン用のlet
文」みたいな位置づけにあるのがlet-else
文になります!パターンにマッチする時はそのまま分割代入され、マッチしない時はelse
節の内容が実行されます。
let 論駁可能パターン = 値 else { never型を返す処理 };
例3のif-let
式を見てみてください。そしてstate_inner
の数を数えてみましょう。
let state_inner = if let MyState::Other(state_inner) = my_state {
state_inner
} else {
println!("Your state is Samumi or Nemumi.");
return;
};
3ですね。3回もstate_inner
と書いています。Otherの中身を取り出したいだけなのに何回もstate_innerを書いてます。このif-let
式の後にstate_inner
を使ったメインの処理が連なることは明白です。この行で主張したいのはelse
節のほうのみです。
let-else
文を使えばこのような悩みから完全に開放されます!
let MyState::Other(state_inner) = my_state else {
println!("Your state is Samumi or Nemumi.");
return;
};
気持ち良すぎだろ!
これはセマンティクス的にもかなり意味が変わっておりより良い書き方になっています。
-
if-let
式: 真の時の枝と偽の時の枝は対等であり、どちらのケースも同じぐらいの重要度を持つ -
let-else
文: 真の時の枝こそが通常時処理であり、else
節の処理は異常系であり普通は起きない
if-let
が必要なシーンではlet-else
のほうが最適なシーンがしばしばありそうです。とてもありがたい構文が追加されました。
注意点: else
節はnever型
let-else
のelse
節では先程説明したnever型を最後に返すようにしなければなりません。つまり、return
やcontinue
、panic!
マクロ等最後に発散する式で締める必要があります。
「じゃあデフォルト値みたいなのを返したい時は?」という順当な疑問が湧くかと思いますが、その時は
-
Result
やOption
型:unwrap_or
を使う - そのほかの列挙型:
if-let
式やmatch
式を使う
といった感じで従来どおりに対応しましょう。
let-else
文のユースケース
実は例1は?
演算子(try!
マクロ)とanyhow::Result
あたりを使って次のように書くのが定石です。
fn main() -> anyhow::Result<()> {
let arg = std::env::args().nth(1).unwrap_or("NaN".to_string());
let val = arg.parse::<i32>()?;
println!("{}^2 = {}", val, val * val);
Ok(())
}
Result
型、Option
型については、「?
(try
マクロ)によってそもそもif-let
やmatch
を使わないで書ける際は?
を使う」のが慣例なのです。
もしエラーが発生した際になにか処理を挟みたい、という場合でも、次のようにmap_err
等で行うことが可能です。
let val = arg.parse::<i32>().map_err(|e| {
println!("エラー時処理");
e
})?;
map_err
で引数を_
にしないで適切に処理すれば例外を握りつぶすことなくエラーハンドリングができます。しかしlet-else
文ではこのような器用なことは不可能です。
同様に考えると、Result
でもOption
でもない列挙体を扱う際はmatch
を使ったほうが見通しが立つシーンが多いでしょう。
おや...?もしかしてlet-else
文要らない...?むしろ let-else
乱用がアンチパターンになる...?
"正常な"異常を返すときには使えそう (例: 404 NOT FOUND
等)
逆に言えばアンチパターンにならなそうなシーンでは使用して良いと思います。「正常な異常」ってなんだよってツッコミが来そうですが、Webサーバーの404のような、要は「原因となる例外を握りつぶして良い、システム側で想定済みのエラー」を表現する際にlet-else
文は見通しを良くしてくれそうです。
struct User {
name: String,
access_count: u64,
}
type UserDict = Arc<Mutex<HashMap<String, User>>>;
async fn search_user(
Path(user_id): Path<String>,
user_dict: Extension<UserDict>,
) -> Result<String, StatusCode> {
let mut user_dict = user_dict.0.lock().unwrap();
let Some(user) = user_dict.get_mut(&user_id) else {
// 見つからないというサーバーにとっては正常な異常
return Err(StatusCode::NOT_FOUND);
};
// 正常時処理
user.access_count += 1;
Ok(format!(
"{} has been accessed {} times",
user.name, user.access_count
))
}
このハンドラはaxumクレートを想定したものです。(ソースコード全文は折りたたんでおきます。)
axumコード全文
use axum::{extract::Path, http::StatusCode, routing::get, Extension, Router};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
struct User {
name: String,
access_count: u64,
}
type UserDict = Arc<Mutex<HashMap<String, User>>>;
async fn search_user(
Path(user_id): Path<String>,
user_dict: Extension<UserDict>,
) -> Result<String, StatusCode> {
let mut user_dict = user_dict.0.lock().unwrap();
let Some(user) = user_dict.get_mut(&user_id) else {
return Err(StatusCode::NOT_FOUND);
};
user.access_count += 1;
Ok(format!(
"{} has been accessed {} times",
user.name, user.access_count
))
}
#[tokio::main]
async fn main() {
let dict: UserDict = Arc::new(Mutex::new(
vec![
(
"user1".to_string(),
User {
name: "Alice".to_string(),
access_count: 0,
},
),
(
"user2".to_string(),
User {
name: "Bob".to_string(),
access_count: 0,
},
),
]
.into_iter()
.collect(),
));
let app = Router::new()
.route("/users/:user_id", get(search_user))
.layer(Extension(dict));
axum::Server::bind(&"0.0.0.0:8000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
この処理は従来ならok_or
やok_or_else
と?
演算子を使って次のように簡潔に書けます。
let user = user_dict.get_mut(&user_id).ok_or(StatusCode::NOT_FOUND)?;
Option
ではなくてResult
の場合でも.map_err(|_| ...)
やthiserrorクレートを使用しやはり?
で事足りてしまうシーンでしょう。
しかしながらこの「正常な異常なのでContext
やバックトレース等を握り潰してよい」ケースでは、?
は単純にreturn
よりも見づらく(5文字少ないですからね)、もしかしたらlet-else
を使って素直に書いたほうが可読性向上につながり良いかもしれません。
...うーん、宗教!(ぜひコメント下さい)
誰もが納得するユースケースを考えてみたいところです。
for
文のcontinue
で欲しい
先述の通り、Result
型やOption
型が返せるシーンでは?
とこれらの型が持つメソッドが有能すぎて、せっかく追加されたlet-else
文は立場がないシーンが多いでしょう。
しかし、for
文のcontinue
は別です。return
してしまう?
では対応できません。let-else
君の就職先あった!やったね!
fn main() {
let maybe_numbers = vec!["0", "1", "2", "fizz", "4", "buzz"];
for maybe_number in maybe_numbers {
let Ok(n) = maybe_number.parse::<i32>() else {
continue;
};
println!("{}^2 = {}", n, n * n);
}
}
...ん?なんですか?あなたは?はい?関数型言語界隈の方?
fn main() {
let maybe_numbers = vec!["0", "1", "2", "fizz", "4", "buzz"];
let pow2numbers: Vec<_> = maybe_numbers
.into_iter()
.filter_map(|maybe_number| maybe_number.parse::<i32>().ok())
.map(|n| n * n)
.collect();
println!("result: {:?}", pow2numbers);
}
...あれ?let-else
君?!どこいったんだ!let-else
君!!!!!!!!
...
...
茶番をしましたが、for
文を忌避する厨二病なRustaceanの皆様におかれましては、滅多なことではfor
文は使いませんよね。失念しておりました。
非同期におけるfor
文のcontinue
で欲しい
関数型大好きRustaceanの皆様を黙らせる納得させる処理に非同期におけるforがあります。await
はmap
やfor_each
とはとても(少なくとも本記事では説明を省略させていただきたいぐらい)相性が悪い1ので、非同期においてはfor
を使うほうが多いと思います。
このようなシーンでcontinue
したい場合に、let-else
文は真価を発揮するのではないでしょうか?!
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
let sites = [
"https://google.com",
"http://localhost:8000",
"https://yahoo.co.jp",
];
for site in sites.iter() {
let Ok(res) = reqwest::get(*site).await else { continue };
println!("{}: {}", site, res.status());
let Ok(body) = res.text().await else { continue };
println!("body len: {}", body.len());
}
Ok(())
}
筆者は書きたくないので書きませんがfor
を使わないで書けた方がいらっしゃいましたらどちらが良いか感想いただければと思います。
ユースケースまとめ
他にも色々考えられると思いますが、とりあえず自分の中ではlet-else
が使えるシーンは次の3点ぐらいかなと考えています。
- ガード節にて"正常な"異常を返す時
- 言い換えると、原因となるエラーが不要で握りつぶしていい時
-
?
よりも可読性向上の可能性があります
-
for
でcontinue
したい時- 特に
for
を使う必要がある非同期
- 特に
- そのほか横着したい時 (下手するとアンチパターン)
- 関数の返り値型を
Result
にしたくなくて、エラー代わりのデフォルト値を返す時- ...
Result
型を検討したほうがいい気がします
- ...
- テストでパニックさせる際に別でなにかやらせたい時
- 関数の返り値型を
使い所を思いついた皆様はlet-else
君のためにもぜひコメントを頂けると幸いです。
まとめ・所感
昔 let-else
文が最も求めていた答えであったであろう Rustで「あ、やっぱいいです」したい - Qiita という記事を書いたのですが、時間が経つにつれやはり関数の返り値をResult
型にし?
を使えるようにするのが良いのでは?と考えるようになっていました。それでもやはり?
よりも読みやすくなるケースや、for
文のcontinue
みたいなResult
が使えないケースが考えられるので、そういうシーンで真価を発揮させるだろうと、今回の記事を書かせていただきました。
最後のユースケースまとめではあまり芳しくないlet-else
文ですが、個人的にはwhile-let
に次いで好きな構文になっています。
ここまで読んでいただき誠にありがとうございました!
引用・参考
- 論駁可能性:パターンが合致しないかどうか - Rust 日本語版
- パターン記法 - The Rust Programming Language 日本語版
- Rustのパターンマッチを完全に理解した | FrozenLib
- 高度な型 - The Rust Programming Language 日本語版
-
https://qiita.com/legokichi/items/4f2c09330f90626600a6 などを見てほしいです。
Future
をcollect
しすべて完了するのを待つみたいな書き方は可能でしょうが、、、高速化させたい等のシーンでないならば素直にループごとにawait
するように書くほうが"早"そうです。 ↩