あっぽログ
← 記事一覧に戻る

RustのクロージャとIteratorを使いこなす:mapやfilterで関数型スタイルの処理を書く

クロージャとは何か

Rustのクロージャ(closure)は、変数に束縛したり引数として渡したりできる「その場で定義できる関数」です。Pythonのラムダ式やJavaScriptのアロー関数に近いイメージです。

基本的な構文は次のとおりです。

fn main() {
    // 通常の関数
    fn add(x: i32, y: i32) -> i32 {
        x + y
    }

    // クロージャ(型推論が効くので型注釈を省略できる)
    let add_closure = |x, y| x + y;

    println!("{}", add(2, 3));        // 5
    println!("{}", add_closure(2, 3)); // 5
}

クロージャが通常の関数と大きく違う点は、周囲のスコープの変数をキャプチャ(捕捉)できることです。

fn main() {
    let offset = 10;

    // offset は外側のスコープの変数だが、クロージャの中で使える
    let add_offset = |x| x + offset;

    println!("{}", add_offset(5));  // 15
    println!("{}", add_offset(20)); // 30
}

クロージャのキャプチャモード

クロージャが外部変数をキャプチャするとき、Rustは状況に応じて自動的にキャプチャ方法を選びます。

キャプチャ方法意味
&T(不変参照)値を読むだけで変更しない
&mut T(可変参照)値を変更する
T(ムーブ)所有権ごと受け取る

明示的に所有権を移したい場合は move キーワードを使います。

fn main() {
    let name = String::from("Rustacean");

    // move で所有権をクロージャに移す
    let greet = move || {
        println!("Hello, {}!", name);
    };

    greet(); // Hello, Rustacean!
    // println!("{}", name); // ← コンパイルエラー: name はムーブ済み
}

move は、スレッドにクロージャを渡すときなどによく使われます。


Iteratorとクロージャを組み合わせる

RustのIteratorトレイトには、クロージャを受け取るメソッドが多数用意されています。これらを組み合わせることで、ループを書かずに宣言的なデータ処理が実現できます。

map:各要素を変換する

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

    // 各要素を2倍にする
    let doubled: Vec<i32> = numbers.iter()
        .map(|&x| x * 2)
        .collect();

    println!("{:?}", doubled); // [2, 4, 6, 8, 10]
}

filter:条件に合う要素だけ残す

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

    // 偶数だけ取り出す
    let evens: Vec<&i32> = numbers.iter()
        .filter(|&&x| x % 2 == 0)
        .collect();

    println!("{:?}", evens); // [2, 4, 6]
}

map と filter を連鎖させる

メソッドチェーンで複数の変換を組み合わせられます。

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

    // 偶数だけ取り出して3倍にする
    let result: Vec<i32> = numbers.iter()
        .filter(|&&x| x % 2 == 0)
        .map(|&x| x * 3)
        .collect();

    println!("{:?}", result); // [6, 12, 18, 24]
}

よく使うIteratorアダプタ

fold:要素を畳み込んで1つの値にまとめる

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

    // 合計を求める(初期値0から始めて足し合わせる)
    let sum = numbers.iter().fold(0, |acc, &x| acc + x);

    println!("合計: {}", sum); // 合計: 15
}

enumerate:インデックスと要素をセットで取得する

fn main() {
    let fruits = vec!["apple", "banana", "cherry"];

    fruits.iter().enumerate().for_each(|(i, fruit)| {
        println!("{}: {}", i, fruit);
    });
    // 0: apple
    // 1: banana
    // 2: cherry
}

flat_map:変換 + 平坦化

fn main() {
    let words = vec!["hello world", "foo bar"];

    // 各文字列をスペースで分割して1つのイテレータにまとめる
    let tokens: Vec<&str> = words.iter()
        .flat_map(|s| s.split_whitespace())
        .collect();

    println!("{:?}", tokens); // ["hello", "world", "foo", "bar"]
}

実践例:構造体のVecを処理する

実際の開発ではstructのVecを扱うことが多いです。

#[derive(Debug)]
struct Product {
    name: String,
    price: u32,
    in_stock: bool,
}

fn main() {
    let products = vec![
        Product { name: "キーボード".to_string(), price: 8000, in_stock: true },
        Product { name: "マウス".to_string(),     price: 3000, in_stock: false },
        Product { name: "モニター".to_string(),   price: 40000, in_stock: true },
        Product { name: "ヘッドセット".to_string(), price: 12000, in_stock: true },
    ];

    // 在庫あり かつ 価格が10000円以上の商品名を取得
    let result: Vec<&str> = products.iter()
        .filter(|p| p.in_stock && p.price >= 10_000)
        .map(|p| p.name.as_str())
        .collect();

    println!("{:?}", result); // ["モニター", "ヘッドセット"]

    // 在庫あり商品の合計金額
    let total: u32 = products.iter()
        .filter(|p| p.in_stock)
        .map(|p| p.price)
        .sum();

    println!("在庫あり合計: {}円", total); // 在庫あり合計: 60000円
}

まとめ

  • クロージャは |引数| 処理 の形で定義し、周囲の変数をキャプチャできる
  • 所有権を明示的に移したいときは move を使う
  • Iterator のアダプタ(map / filter / fold / flat_map など)とクロージャを組み合わせると、ループを書かずに簡潔なデータ処理ができる
  • 最後に .collect() を呼ぶことでVecなどのコレクションに変換できる

Rustのイテレータは遅延評価(lazy evaluation)なので、チェーンを組んでも中間コレクションは生成されません。パフォーマンスを損なわずに読みやすいコードが書けるのは大きな魅力です。ぜひ積極的に活用してみてください!

← 記事一覧に戻る