blog.dopana

Back

中級Rustシリーズの14回目です。スマートポインタをマスターした後、Rustの関数型プログラミング機能について学びます。Rustは関数型言語から多くの概念を借りています:クロージャ(匿名関数)とイテレータ(遅延評価、合成可能なシーケンス)を提供します。その重要な約束はゼロコスト抽象化 — これらの高レベル構造は手書きのループと同じ機械コードにコンパイルされます。

graph TD
    A["関数型Rust"] --> B["クロージャ<br/>Fn, FnMut, FnOnce"]
    A --> C["イテレータ<br/>遅延評価"]
    A --> D["ゼロコスト抽象化"]
    A --> E["map, filter, fold"]

クロージャ:匿名関数#

クロージャは、その周囲の環境をキャプチャできる匿名関数です。通常の関数とは異なり、クロージャは周囲のスコープから変数を「閉じ込む」(close over)ことができます。

基本構文#

fn main() {
    let add_one = |x: i32| x + 1;
    println!("結果: {}", add_one(5)); // 結果: 6

    // クロージャは環境をキャプチャできる
    let multiplier = 3;
    let multiply = |x: i32| x * multiplier;
    println!("結果: {}", multiply(5)); // 結果: 15
}
rust

|x: i32| の構文は、他の言語のクロージャと同様にパラメータリストを定義します。

環境のキャプチャ#

クロージャは環境から変数を3つの方法でキャプチャします:

  1. Fn — 参照でキャプチャ(&T
  2. FnMut — 可変参照でキャプチャ(&mut T
  3. FnOnce — 値でキャプチャ(所有権の移動)

[!NOTE] FnOnce を実装するクロージャは、FnMutFn としても使用できますが、その逆はできません。コンパイラは必要な最小限のトレイトを推論します。

関数の引数としてのクロージャ#

クロージャは、高階関数の引数として渡されると特に強力になります:

fn apply_to_list<T>(list: &[T], f: impl Fn(&T)) {
    for item in list {
        f(item);
    }
}

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    apply_to_list(&numbers, |x| println!("数字: {x}"));
}
rust

戻り値としてのクロージャ#

impl Fn を使用して、関数からクロージャを返すこともできます:

fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
    |x| x * factor
}

fn main() {
    let double = make_multiplier(2);
    let triple = make_multiplier(3);

    println!("2倍: {}", double(5)); // 2倍: 10
    println!("3倍: {}", triple(5)); // 3倍: 15
}
rust

イテレータ:遅延シーケンス#

イテレータは一連の値を生成する仕組みです。Rustでは、イテレータは遅延評価 — collect()for のような消費メソッドを呼び出すまで何もしません。

イテレータの作成#

Rustのすべてのコレクションは .iter() メソッドを持ち、イテレータを返します:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    // .iter() はイテレータを返す
    let iter = numbers.iter();

    // イテレータは遅延評価 — まだ何も起きていない
    for num in iter {
        println!("{num}");
    }
}
rust

イテレータアダプタ#

イテレータアダプタは、イテレータを受け取り、新しいイテレータを返します。最も一般的なものは mapfilterfold です:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    let result: Vec<i32> = numbers
        .iter()
        .filter(|&&x| x % 2 == 0)   // 偶数を保持
        .map(|&x| x * 2)           // 2倍にする
        .collect();                // Vecに収集

    println!("{:?}", result); // [4, 8, 12, 16, 20]
}
rust
graph LR
    A["[1,2,3,4,5,6,7,8,9,10]"] --> B["filter: even"]
    B --> C["[2,4,6,8,10]"]
    C --> D["map: x2"]
    D --> E["[4,8,12,16,20]"]

fold メソッド#

fold は、アキュムレータを持つ for ループの関数型版です:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    // 合計を計算
    let sum: i32 = numbers.iter().fold(0, |acc, &x| acc + x);
    println!("合計: {sum}"); // 合計: 15

    // 最大値を見つける
    let max = numbers.iter().fold(0, |acc, &x| if x > acc { x } else { acc });
    println!("最大値: {max}"); // 最大値: 5
}
rust

impl Iterator によるカスタムイテレータ#

Iterator トレイトを実装することで、独自のイテレータを作成できます:

ゼロコスト抽象化#

Rustの関数型機能の特徴は、手書きのループと同じ機械コードにコンパイルされることです。証明しましょう:

// 関数型スタイル
fn functional_sum(numbers: &[i32]) -> i32 {
    numbers.iter().filter(|&&x| x > &0).map(|&x| x * 2).sum()
}

// 命令型スタイル
fn imperative_sum(numbers: &[i32]) -> i32 {
    let mut sum = 0;
    for &x in numbers {
        if x > 0 {
            sum += x * 2;
        }
    }
    sum
}
rust

これらの関数は同じアセンブリコードを生成します。コンパイラはイテレータチェーンを完全に最適化し去ります。

[!TIP] Rustのイテレータはゼロコスト抽象化の典型的な例です。手動最適化されたCのパフォーマンスと関数型プログラミングの表現力の両方を手に入れられます。

分かりやすい例え(ELI5):工場のコンベアベルト#

イテレータを工場のコンベアベルトに例えて考えてみましょう:

  • 原材料(元のコレクション)がコンベアの開始点に到達します。
  • 各アダプタ(mapfilter など)は、アイテムを変換または削除するワークステーションです。
  • コンベアベルト(イテレータ)は遅延評価 — アイテムは終点の作業員が引っ張るときに初めて動きます。
  • 最終消費者(collectsumfor)は、完成品をベルトから取り外す作業員です。

ベルトが遅延評価なので、作業員が3つのアイテムしか必要としない場合、工場は3つしか処理しません — 余分なエネルギーを無駄にしません!

まとめ#

  • クロージャは環境をキャプチャできる匿名関数で、3つのキャプチャモード(FnFnMutFnOnce)があります。
  • イテレータは遅延評価のシーケンスで、mapfilterfold のような強力なアダプタを備えています。
  • ゼロコスト抽象化は、関数型スタイルのコードが命令型ループと同じ機械コードにコンパイルされることを意味します。
  • Iterator トレイトを実装することで、カスタムイテレータを作成できます。

次回の記事では、Fearless Concurrency について掘り下げます — スレッド、チャネル、共有状態を使った安全な並列プログラミングのRustのアプローチ。

参照#