課題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
提出期限
---
ヒント
問題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
---
学習の確認
この課題を通じて、以下を理解できたか確認してください:
次の章では、スマートポインタについて学びます。Box、Rc、Arcなど、高度なメモリ管理技術を理解します。