課題14: クロージャ (Closures)

マンダトリー要件

問題1: クロージャの基本(20点)

以下の関数を実装しなさい。

fn main() {
    // TODO: 以下のクロージャを実装

    // 1. 引数を2倍にするクロージャ (5点)
    let double = // ここを実装

    // 2. 2つの引数を加算するクロージャ (5点)
    let add = // ここを実装

    // 3. 文字列を大文字に変換するクロージャ (5点)
    let to_uppercase = // ここを実装

    // 4. 環境の変数をキャプチャして使うクロージャ (5点)
    let multiplier = 10;
    let multiply_by_ten = // ここを実装(multiplierを使用)

    // テスト
    assert_eq!(double(5), 10);
    assert_eq!(add(3, 7), 10);
    assert_eq!(to_uppercase("hello"), "HELLO");
    assert_eq!(multiply_by_ten(5), 50);

    println!("All tests passed!");
}

---

問題2: Fn, FnMut, FnOnce の理解(30点)

以下の関数を実装し、適切なトレイト境界を使いなさい。

// 1. Fnトレイトを使う関数 (10点)
// クロージャを2回呼び出す
fn call_twice<F>(f: F, arg: i32) -> (i32, i32)
where
    // TODO: トレイト境界を実装
{
    // TODO: 実装
}

// 2. FnMutトレイトを使う関数 (10点)
// クロージャを複数回呼び出し、結果を収集
fn call_n_times<F>(mut f: F, n: usize) -> Vec<i32>
where
    // TODO: トレイト境界を実装
{
    // TODO: 実装
}

// 3. FnOnceトレイトを使う関数 (10点)
// クロージャを一度だけ呼び出す
fn call_once<F>(f: F) -> String
where
    // TODO: トレイト境界を実装
{
    // TODO: 実装
}

fn main() {
    // テスト1: Fn
    let add_ten = |x| x + 10;
    let (r1, r2) = call_twice(add_ten, 5);
    assert_eq!((r1, r2), (15, 15));

    // テスト2: FnMut
    let mut counter = 0;
    let mut increment = || {
        counter += 1;
        counter
    };
    let results = call_n_times(&mut increment, 5);
    assert_eq!(results, vec![1, 2, 3, 4, 5]);

    // テスト3: FnOnce
    let s = String::from("Hello, World!");
    let consume = || s;
    let result = call_once(consume);
    assert_eq!(result, "Hello, World!");

    println!("All tests passed!");
}

---

問題3: 関数型プログラミングパターン(30点)

以下のデータ処理パイプラインを実装しなさい。

#[derive(Debug, Clone)]
struct Product {
    name: String,
    price: f64,
    category: String,
    stock: u32,
}

fn main() {
    let products = vec![
        Product { name: "Laptop".to_string(), price: 1000.0, category: "Electronics".to_string(), stock: 5 },
        Product { name: "Mouse".to_string(), price: 25.0, category: "Electronics".to_string(), stock: 50 },
        Product { name: "Desk".to_string(), price: 200.0, category: "Furniture".to_string(), stock: 10 },
        Product { name: "Chair".to_string(), price: 150.0, category: "Furniture".to_string(), stock: 15 },
        Product { name: "Monitor".to_string(), price: 300.0, category: "Electronics".to_string(), stock: 8 },
    ];

    // TODO: 以下の関数を実装

    // 1. カテゴリでフィルタリングして価格の合計を計算 (10点)
    let electronics_total = calculate_category_total(&products, "Electronics");
    assert_eq!(electronics_total, 1325.0);

    // 2. 価格の降順でソートして上位N件を取得 (10点)
    let top_3 = get_top_n_by_price(&products, 3);
    assert_eq!(top_3.len(), 3);
    assert_eq!(top_3[0].name, "Laptop");

    // 3. 在庫切れ間近(在庫10以下)の商品名を取得 (10点)
    let low_stock = get_low_stock_products(&products, 10);
    assert_eq!(low_stock.len(), 3);

    println!("All tests passed!");
}

fn calculate_category_total(products: &[Product], category: &str) -> f64 {
    // TODO: イテレータとクロージャを使って実装
}

fn get_top_n_by_price(products: &[Product], n: usize) -> Vec<Product> {
    // TODO: イテレータとクロージャを使って実装
    // ヒント: sorted_by を使う(Vec::sort_by でも可)
}

fn get_low_stock_products(products: &[Product], threshold: u32) -> Vec<String> {
    // TODO: イテレータとクロージャを使って実装
}

---

ボーナス課題

ボーナス1: 高階関数の実装(10点)

関数を返す関数を実装しなさい。

// 指定された演算を行う関数を返す
fn create_operation(op: &str) -> Box<dyn Fn(i32, i32) -> i32> {
    // TODO: 実装
    // "add", "sub", "mul", "div" をサポート
}

fn main() {
    let add = create_operation("add");
    let mul = create_operation("mul");

    assert_eq!(add(5, 3), 8);
    assert_eq!(mul(5, 3), 15);

    println!("Bonus 1 passed!");
}

評価基準:

  • 基本的な演算の実装: 5点
  • エラーハンドリング(不明な演算子): 3点
  • コードの簡潔性: 2点

---

ボーナス2: カスタムイテレータアダプタ(10点)

TakeWhileのような独自のアダプタを実装しなさい。

// 条件を満たす間だけ要素を返すイテレータアダプタ
struct TakeWhile<I, P>
where
    I: Iterator,
    P: FnMut(&I::Item) -> bool,
{
    iter: I,
    predicate: P,
    done: bool,
}

impl<I, P> TakeWhile<I, P>
where
    I: Iterator,
    P: FnMut(&I::Item) -> bool,
{
    fn new(iter: I, predicate: P) -> Self {
        // TODO: 実装
    }
}

impl<I, P> Iterator for TakeWhile<I, P>
where
    I: Iterator,
    P: FnMut(&I::Item) -> bool,
{
    type Item = I::Item;

    fn next(&mut self) -> Option<Self::Item> {
        // TODO: 実装
    }
}

// 拡張トレイト
trait IteratorExt: Iterator {
    fn take_while_custom<P>(self, predicate: P) -> TakeWhile<Self, P>
    where
        Self: Sized,
        P: FnMut(&Self::Item) -> bool,
    {
        TakeWhile::new(self, predicate)
    }
}

impl<I: Iterator> IteratorExt for I {}

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

    let result: Vec<i32> = numbers.iter()
        .copied()
        .take_while_custom(|&x| x < 4)
        .collect();

    assert_eq!(result, vec![1, 2, 3]);

    println!("Bonus 2 passed!");
}

評価基準:

  • TakeWhileの基本実装: 5点
  • predicateの正しい使用: 3点
  • 拡張トレイトの実装: 2点

---

ボーナス3: メモ化(Memoization)(10点)

関数の結果をキャッシュするメモ化機能を実装しなさい。

use std::collections::HashMap;
use std::hash::Hash;

struct Memoized<F, A, R>
where
    F: FnMut(A) -> R,
    A: Eq + Hash + Clone,
    R: Clone,
{
    function: F,
    cache: HashMap<A, R>,
}

impl<F, A, R> Memoized<F, A, R>
where
    F: FnMut(A) -> R,
    A: Eq + Hash + Clone,
    R: Clone,
{
    fn new(function: F) -> Self {
        // TODO: 実装
    }

    fn call(&mut self, arg: A) -> R {
        // TODO: 実装
        // キャッシュにあれば返す、なければ計算してキャッシュに保存
    }
}

// フィボナッチ数(再帰版)
fn fibonacci(n: u64) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        n => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

fn main() {
    let mut memo_fib = Memoized::new(|n: u64| fibonacci(n));

    // 最初の呼び出し(計算する)
    let result1 = memo_fib.call(10);
    println!("Result: {}", result1);

    // 2回目の呼び出し(キャッシュから返す)
    let result2 = memo_fib.call(10);
    assert_eq!(result1, result2);

    // 性能テスト
    use std::time::Instant;

    let start = Instant::now();
    let _ = memo_fib.call(30);
    let elapsed1 = start.elapsed();

    let start = Instant::now();
    let _ = memo_fib.call(30);  // キャッシュヒット
    let elapsed2 = start.elapsed();

    println!("First call: {:?}", elapsed1);
    println!("Cached call: {:?}", elapsed2);
    assert!(elapsed2 < elapsed1);

    println!("Bonus 3 passed!");
}

評価基準:

  • 基本的なメモ化機能: 5点
  • キャッシュの正しい管理: 3点
  • 性能の向上が確認できる: 2点

---

ボーナス4: カスタムコンビネータ(10点)

独自のコンビネータ関数を実装しなさい。

// andThen: 2つの関数を合成
fn and_then<A, B, C, F, G>(f: F, g: G) -> impl Fn(A) -> C
where
    F: Fn(A) -> B,
    G: Fn(B) -> C,
{
    // TODO: 実装
}

// or_else: 失敗時のフォールバック
fn or_else<T, E, F, G>(f: F, fallback: G) -> impl Fn(T) -> Result<T, E>
where
    F: Fn(T) -> Result<T, E>,
    G: Fn(E) -> Result<T, E>,
    T: Clone,
    E: Clone,
{
    // TODO: 実装
}

fn main() {
    // andThenのテスト
    let add_one = |x: i32| x + 1;
    let double = |x: i32| x * 2;
    let composed = and_then(add_one, double);

    assert_eq!(composed(5), 12);  // (5 + 1) * 2

    // or_elseのテスト
    let parse = |s: &str| s.parse::<i32>().map_err(|_| "Parse error");
    let fallback = |_| Ok(0);
    let safe_parse = or_else(parse, fallback);

    assert_eq!(safe_parse("42").unwrap(), 42);
    assert_eq!(safe_parse("invalid").unwrap(), 0);

    println!("Bonus 4 passed!");
}

評価基準:

  • and_thenの実装: 5点
  • or_elseの実装: 5点
  • ---

    評価基準

    マンダトリー部分(80点)

    項目 配点 評価ポイント
    問題1:基本構文 20点 クロージャの正しい使用
    問題2:トレイト理解 30点 Fn/FnMut/FnOnceの適切な選択
    問題3:関数型パターン 30点 イテレータとの組み合わせ

    ボーナス部分(20点)

    項目 配点 評価ポイント
    ボーナス1:高階関数 10点 関数を返す関数の実装
    ボーナス2:アダプタ 10点 カスタムイテレータアダプタ
    ボーナス3:メモ化 10点 キャッシング機能の実装
    ボーナス4:コンビネータ 10点 関数合成の実装

    : ボーナスは最大20点まで加算されます。

    ---

    提出方法

    ファイル構成

    rust-foundations-14/
    ├── src/
    │   ├── problem1.rs
    │   ├── problem2.rs
    │   ├── problem3.rs
    │   ├── bonus1.rs       # オプション
    │   ├── bonus2.rs       # オプション
    │   ├── bonus3.rs       # オプション
    │   └── bonus4.rs       # オプション
    ├── Cargo.toml
    └── README.md
    

    テスト

    cargo test --release
    

    提出期限

  • マンダトリー:第14章学習後、1週間以内
  • ボーナス:第16章修了時まで
  • ---

    ヒント

    問題2のヒント

    // Fn: 環境を変更しない
    F: Fn(i32) -> i32
    
    // FnMut: 環境を変更する
    F: FnMut() -> i32
    
    // FnOnce: 所有権を消費する可能性
    F: FnOnce() -> String
    

    問題3のヒント

    // 価格の合計
    products.iter()
        .filter(|p| p.category == category)
        .map(|p| p.price)
        .sum()
    
    // ソート
    let mut sorted = products.to_vec();
    sorted.sort_by(|a, b| b.price.partial_cmp(&a.price).unwrap());
    

    ボーナス3のヒント

    // キャッシュの確認
    if let Some(result) = self.cache.get(&arg) {
        return result.clone();
    }
    
    // 計算してキャッシュに保存
    let result = (self.function)(arg.clone());
    self.cache.insert(arg, result.clone());
    result
    

    ---

    学習の確認

    この課題を通じて、以下を理解できたか確認してください:

  • [ ] クロージャの基本構文
  • [ ] 環境のキャプチャ(不変、可変、所有権)
  • [ ] Fn、FnMut、FnOnceトレイトの違い
  • [ ] moveキーワードの使い方
  • [ ] 高階関数の実装
  • [ ] 関数型プログラミングパターン
  • [ ] イテレータとクロージャの組み合わせ

次の章では、スマートポインタについて学びます。Box、Rc、Arcなど、高度なメモリ管理技術を理解します。