Rustのlet-else文気持ち良すぎだろ

先日、Rust バージョン1.65.0が利用できるようになりました :cracker:

https://blog.rust-lang.org/2022/11/03/Rust-1.65.0.html

https://aznhe21.hatenablog.com/entry/2022/11/04/rust-1.65

その中でも個人的に最も嬉しい機能追加が`let-else`文になります！

# `let-else`文の見た目

`let-else`文はこんな見た目をしています。

```rust: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`"式"があれば争いなんて起きないのに...(ﾄｵｲﾒ」といつも思っています。

```rust:Rust
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`型

```rust:例1
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`にはもともと構造体やタプルを展開して束縛する機能、いわゆる[分割代入](https://typescript-jp.gitbook.io/deep-dive/future-javascript/destructuring)があり、列挙体等複数可能性があるパターン(これを論駁可能なパターンといいます)については`if-let`式で受け取れるようにした、という感じでしょうか。そして`match`式は論駁可能パターンに対して複数のパターンマッチを行い分岐する`if-let`式の強化版になっています。

例2: 構造体の分割代入

```rust:例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: 任意の列挙型

```rust:例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`式を使うべきでしょう。

```rust:例3'
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`節の内容が実行されます。

```rust:let-else文
let 論駁可能パターン = 値 else { never型を返す処理 };
```

例3の`if-let`式を見てみてください。そして`state_inner`の数を数えてみましょう。

```rust
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`文を使えばこのような悩みから完全に開放されます！

```rust
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`あたりを使って次のように書くのが定石です。

```rust:例1'
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`等で行うことが可能です。

```rust:例1''
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`文は見通しを良くしてくれそうです。
 
```rust:Rust
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クレート](https://docs.rs/axum/latest/axum/)を想定したものです。(ソースコード全文は折りたたんでおきます。)

<details><summary>axumコード全文</summary><div>

```rust:Rust
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();
}
```
</div></details>

この処理は従来なら`ok_or`や`ok_or_else`と`?`演算子を使って次のように簡潔に書けます。

```rust:Rust
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`君の就職先あった！やったね！

```rust:例4
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);
    }
}
```

...ん？なんですか？あなたは？はい？関数型言語界隈の方？

```rust:例4'
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`を使うほうが多いと思います。

[^1]: https://qiita.com/legokichi/items/4f2c09330f90626600a6 などを見てほしいです。`Future`を`collect`しすべて完了するのを待つみたいな書き方は可能でしょうが、、、高速化させたい等のシーンでないならば素直にループごとに`await`するように書くほうが"早"そうです。

このようなシーンで`continue`したい場合に、`let-else`文は真価を発揮するのではないでしょうか？！

```rust:例5
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](https://qiita.com/namn1125/items/0ace037da7c29f0c6140) という記事を書いたのですが、時間が経つにつれやはり関数の返り値を`Result`型にし`?`を使えるようにするのが良いのでは？と考えるようになっていました。それでもやはり`?`よりも読みやすくなるケースや、`for`文の`continue`みたいな`Result`が使えないケースが考えられるので、そういうシーンで真価を発揮させるだろうと、今回の記事を書かせていただきました。

最後のユースケースまとめではあまり芳しくない`let-else`文ですが、個人的には`while-let`に次いで好きな構文になっています。

ここまで読んでいただき誠にありがとうございました！

# 引用・参考

- [論駁可能性：パターンが合致しないかどうか - Rust 日本語版](https://man.plustar.jp/rust/book/ch18-02-refutability.html)
- [パターン記法 - The Rust Programming Language 日本語版](https://doc.rust-jp.rs/book-ja/ch18-03-pattern-syntax.html)
- [Rustのパターンマッチを完全に理解した | FrozenLib](https://frozenlib.net/blog/2018-03-11_rust-pattern-match/)
- [高度な型 - The Rust Programming Language 日本語版](https://doc.rust-jp.rs/book-ja/ch19-04-advanced-types.html#never%E5%9E%8B%E3%81%AF%E7%B5%B6%E5%AF%BE%E3%81%AB%E8%BF%94%E3%82%89%E3%81%AA%E3%81%84)