はじめに
Rust の let-else 構文は、Rust 1.65 (2022年) で安定化された機能です。古くからある機能ですが、2024 edition のクリーンなコードベースでは多用されます。clippy の manual_let_else ルールも 2024 では使うことが推奨されます。
「Option が None だったら関数を抜ける」「Result が Err だったらエラーで早期 return する」というパターンを、ネストせず書ける構文です。
何を解決するか
Option<T> から T を取り出して処理を続けたい、という場面はたくさんあります。素直に書くとこうなります。
fn label(maybe_name: Option<&str>) -> Option<String> { if let Some(name) = maybe_name { Some(format!("hello, {name}")) } else { None } }
これは match でも同じです。
fn label(maybe_name: Option<&str>) -> Option<String> { match maybe_name { Some(name) => Some(format!("hello, {name}")), None => None, } }
短ければこれで困りませんが、続きの処理が長くなるとネストが深くなります。
fn process(maybe_name: Option<&str>, maybe_age: Option<u32>) -> Option<String> { if let Some(name) = maybe_name { if let Some(age) = maybe_age { // 本来やりたい処理がインデントの底に沈む Some(format!("{name} is {age}")) } else { None } } else { None } }
ネストが2段、3段と増えると、何を達成したいのかコードからすぐに読み取れなくなります。
let-else の書き方
let-else は「let で値を取り出すが、失敗したら else ブロックでスコープから抜ける」と書けます。
fn label(maybe_name: Option<&str>) -> Option<String> { let Some(name) = maybe_name else { return None; }; Some(format!("hello, {name}")) }
ポイントを整理します。
let Some(name) = maybe_nameで名前を取り出そうとする- 失敗 (
None) ならelseブロックに入る elseブロックは 必ず関数から抜ける (return, break, continue, panic) 必要がある- 成功した場合、
nameは変数として以降のスコープで使える
ネストの段数が減って、本来やりたい処理 (=フォーマットして返す) が一直線に書けます。
ネスト解消の効果
先ほどの2段ネスト版を let-else で書き直してみます。
fn process(maybe_name: Option<&str>, maybe_age: Option<u32>) -> Option<String> { let Some(name) = maybe_name else { return None; }; let Some(age) = maybe_age else { return None; }; Some(format!("{name} is {age}")) }
3行になりました。「失敗ケースは早めに弾いて、本筋のコードを平らに書く」 という guard pattern が自然に表現できます。
Result でも同じ
Result でも同じ書き方ができます。エラー伝搬は ? のほうが短いですが、エラー型を変換したい時は let-else のほうが明示的です。
#[derive(Debug)] enum AppError { InvalidInput(String), } fn parse_pair(s: &str) -> Result<(i32, i32), AppError> { let Some((left, right)) = s.split_once(',') else { return Err(AppError::InvalidInput(format!("missing comma: {s}"))); }; let Ok(left) = left.trim().parse::<i32>() else { return Err(AppError::InvalidInput(format!("not an int: {}", left.trim()))); }; let Ok(right) = right.trim().parse::<i32>() else { return Err(AppError::InvalidInput(format!("not an int: {}", right.trim()))); }; Ok((left, right)) } fn main() { println!("{:?}", parse_pair("1, 2")); println!("{:?}", parse_pair("hello")); println!("{:?}", parse_pair("1, x")); }
実行結果はこうなります。
Ok((1, 2))
Err(InvalidInput("missing comma: hello"))
Err(InvalidInput("not an int: x"))
これを ? で書こうとすると、 String を AppError に変換する From 実装を毎回足す必要が出てきます。let-else ならエラーメッセージを箇所ごとに直接書けるので、テスト書く前のプロトタイプ段階では let-else のほうが速いです。
何を else ブロックに書けるか
else ブロックは「never 型を返す」必要があります。具体的には次のような選択肢です。
return ...breakcontinuepanicマクロloopで永久ループ- 上記を返す関数 (例:
std::process::exit)
普通の値を返すと「else ブロックは関数から抜ける必要があります」とコンパイラに怒られます。else ブロック内で何か値を計算してから抜けるのは OK です。
let Some(name) = maybe_name else { eprintln!("name is missing"); return None; };
clippy::manual_let_else で機械検出する
Cargo.toml に1行入れると、let-else で書ける箇所を clippy が指摘してくれます。
[lints.clippy] manual_let_else = "warn"
新規プロジェクトでは入れておくのを勧めます。if let { ... } else { return ... } を見つけて警告してくれるので、自然に let-else の習慣がつきます。
いつ使わないか
let-else がいつでも正解というわけではありません。以下のケースは別の構文のほうが自然です。
- 成功時と失敗時で同じ後処理がある:
matchのほうが両分岐を並べて書けて読みやすい Optionを別の値に置き換えたいだけ:unwrap_or/unwrap_or_else/map_orで十分ResultのErrをそのまま伝搬したいだけ:?で1文字
// これは let-else よりシンプル let port = config.get("port").unwrap_or("8080"); // これは let-else より ? が自然 fn read() -> Result<String, std::io::Error> { let content = std::fs::read_to_string("config.toml")?; Ok(content) }
let-else は 「失敗時は関数から抜けるが、成功時は以降長く処理を続ける」 ときに最も光ります。
まとめ
let-elseはネストを減らして guard pattern を書ける構文elseブロックはreturn/break/panic!などで必ず抜ける必要があるOption/Resultのどちらにも使えるclippy::manual_let_else = "warn"で書き直し候補を機械検出できる- 失敗パスが短く、成功パスが長い場面で最大の効果
関連
let-elseRFC: https://rust-lang.github.io/rfcs/3137-let-else.html- clippy
manual_let_else: https://rust-lang.github.io/rust-clippy/master/index.html#manual_let_else