じゃあ、おうちで学べる

本能を呼び覚ますこのコードに、君は抗えるか

`let-else` で早期 return を素直に書く

はじめに

Rust の let-else 構文は、Rust 1.65 (2022年) で安定化された機能です。古くからある機能ですが、2024 edition のクリーンなコードベースでは多用されます。clippy の manual_let_else ルールも 2024 では使うことが推奨されます。

OptionNone だったら関数を抜ける」「ResultErr だったらエラーで早期 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"))

これを ? で書こうとすると、 StringAppError に変換する From 実装を毎回足す必要が出てきます。let-else ならエラーメッセージを箇所ごとに直接書けるので、テスト書く前のプロトタイプ段階では let-else のほうが速いです。

何を else ブロックに書けるか

else ブロックは「never 型を返す」必要があります。具体的には次のような選択肢です。

  • return ...
  • break
  • continue
  • panic マクロ
  • 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 で十分
  • ResultErr をそのまま伝搬したいだけ: ? で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" で書き直し候補を機械検出できる
  • 失敗パスが短く、成功パスが長い場面で最大の効果

関連