
2025/12/19 13:56
Rust's Block Pattern
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
(以下は元の文章を日本語に翻訳したものです)
要約
Rust のブロックは値を返す式として使うことができ、
{ … } を用いてロジックをカプセル化し、単一の結果を生成できます。記事では簡単な例でこれを示しています。
let foo = { let x = 1; let y = 2; x + y };
その後、本番環境で使用される設定読み込みルーチンを
regex、serde_json、および標準ライブラリ関数を用いた一つのブロックにリファクタリングします。
let config = { static STRIP_COMMENTS: LazyLock<Regex> = /* … */; let raw_data = fs::read(cfg_file)?; let data_str = String::from_utf8(&raw_data)?; let stripped = STRIP_COMMENTS.replace(&data_str, ""); serde_json::from_str(&stripped)? };
主なメリット
- 意図の明確化 – 先頭の
が設定が生成されることを示します。let config = … - 名前空間の整頓 – 中間変数 (
,raw_data
,data_str
) はブロック内でスコープされ、終了時に自動的にドロップされます。stripped - 可変性の消去 – 一時ベクタなどの可変状態はブロック内だけに留まり、ブロックが返った後は外側のバインディングが不変になり、他所で変更できなくなります。
- インライン便利さ – 多数のパラメータを必要とするヘルパー関数を抽出する代わりに、ブロックはすべてのロジックをインラインに保ちつつ、残りの関数をクリーンに維持します。
著者はこの構文を「ブロックパターン」と呼び、Rust コミュニティ内で非公式な名称が既に存在する可能性があると指摘しています。この手法を採用すると、設定ファイルに大きく依存するプロジェクトの可読性と安全性が向上し、露出される可変状態を制限し、名前空間の混乱を減らすことができます。
本文
以下は、あまり語られないようなイディオムですが、Rust コードをよりクリーンで頑丈にする効果があると感じています。
実際にこのパターンに名前が付いているかどうかは分かりませんので、「ブロック・パターン」という名称で呼んでいます。頻繁に利用しており、他の Rust プロジェクトでも採用すればコードが整然とすると思います。
もし既存の名前がある場合は教えてください。
ブロック・パターンとは?
Rust のブロック
{ … } は有効な式です。そのため、ブロックを評価した結果を変数に代入できます。
let foo = { let x = 1; let y = 2; x + y // ← ブロックの最後の式が返り値になる };
上記は
let foo = 3;
と同等です。
なぜ重要なのか?
たとえば、設定ファイルを読み込み、その内容に応じて複数の HTTP リクエストを送る関数があるとします。
設定をロードするには以下の手順が必要です。
- ファイルから生データ(バイト列)を読む
- コメントを除去(例:コメント付き JSON)
- 生成した文字列を JSON としてパース
単純に実装すると次のようになります。
use regex::{Regex, RegexBuilder}; use std::{fs, sync::LazyLock}; #[derive(serde::Deserialize)] struct Config { /* ... */ } static STRIP_COMMENTS: LazyLock<Regex> = LazyLock::new(|| { RegexBuilder::new(r"//.*") .multi_line(true) .build() .expect("regex build failed") }); fn foo(cfg_file: &str) -> anyhow::Result<()> { let config_data = fs::read(cfg_file)?; let config_string = String::from_utf8(&config_data)?; let stripped_data = STRIP_COMMENTS.replace(&config_string, ""); let config = serde_json::from_str(&stripped_data)?; send_http_request(&config.url1)?; send_http_request(&config.url2)?; send_http_request(&config.url3)?; Ok(()) }
このコードは動作しますが、いくつか欠点があります。
- 名前空間の汚染 –
・config_data
・config_string
・stripped_data
という四つの一時変数が関数内に残る。config
解析後は
のみが必要です。config - 意図が隠れている – 読む側は何行も読むまで「最終的に Config を作り出す」という目的を理解できません。
- 誤用のリスク – 一時変数は可変 (
) であるため、後から偶然変更される恐れがあります。mut
ブロックでロジックをカプセル化
ブロック・パターンでは、すべての中間ステップを一つの式内に収めることで上記問題を解決します。
fn foo(cfg_file: &str) -> anyhow::Result<()> { let config = { // コメント除去用正規表現(キャッシュ済み) static STRIP_COMMENTS: LazyLock<Regex> = LazyLock::new(|| { RegexBuilder::new(r"//.*") .multi_line(true) .build() .expect("regex build failed") }); let raw_data = fs::read(cfg_file)?; let data_string = String::from_utf8(&raw_data)?; let stripped_data = STRIP_COMMENTS.replace(&data_string, ""); serde_json::from_str(&stripped_data)? }; send_http_request(&config.url1)?; send_http_request(&config.url2)?; send_http_request(&config.url3)?; Ok(()) }
メリット
- 意図が明確 –
という代入文だけで「ここは設定オブジェクトを生成するためのブロックだ」と即座にわかります。let config = { … } - 名前空間の衛生性 –
・raw_data
など中間変数はブロック内に限定され、関数の他の部分からはアクセスできません。data_string - 自動クリーンアップ – ブロックが終わるとそのスコープ内の値はすべてドロップし、リソースが解放されます。
別案:別関数へ抽出
同じ効果を得るには、上記ロジックを独立したヘルパー関数に切り出す方法もあります。
しかしその場合、次の欠点があります。
- コードフローが線形でなくなるため、読者は別ファイルや別場所へジャンプしなければならない。
- 中間値を他で使う必要がある場合は、すべてパラメータとして渡さねばならない。
ブロック・パターンは、インラインの可読性を保ちつつ一時変数を隔離できる点で優れています。
可変性の削減
さらに微妙なメリットとして、ブロックは可変性を限定できます。
let data = { let mut temp = vec![]; temp.push(1); temp.extend_from_slice(&[4, 5, 6, 7]); temp // ブロックの戻り値は不変になる }; data.iter().for_each(|x| println!("{x}")); return data[2];
ブロック内では
temp が可変ですが、外側に出ると不変な Vec<i32> になります。これにより初期化フェーズ以降の意図しない変更を防げます。
終わりに
このパターンが Rust コミュニティで既に名前付けされているかは不明ですが、共有する価値はあると考えています。
関数を簡潔に保ち、意図を明示し、予期せぬ副作用から守るための手段として、ぜひ採用してみてください。