課題19: マクロとDSL作成
マンダトリー要件
問題1:基本的なマクロ理解(15点)
以下の質問に答えなさい。
- マクロと関数の違い(5点)
println!は関数ではなくマクロなのか?- メタ変数の種類(5点)
$x:expr
- $t:ty
- $i:ident
- $b:block
- $p:pat- 繰り返しパターン(5点)
$(...)
- $(...)+
- $(...),
- $(...),+問題2:マクロ実装(30点)
以下のマクロを実装しなさい。
2.1 min!マクロ(10点)
可変長引数の中から最小値を返すマクロを実装してください。
macro_rules! min {
// ここに実装
}
fn main() {
assert_eq!(min!(3), 3);
assert_eq!(min!(3, 5), 3);
assert_eq!(min!(3, 5, 1, 4), 1);
assert_eq!(min!(10, -5, 0, 7), -5);
}
要件:
- 1つ以上の引数を受け取る(0個はコンパイルエラー)
- 任意の比較可能な型で動作する
- 再帰的な実装でも反復的な実装でも可
2.2 log!マクロ(10点)
ログレベル付きのロギングマクロを実装してください。
macro_rules! log {
// ここに実装
}
fn main() {
log!(INFO, "アプリケーション起動");
log!(WARN, "メモリ使用量: {}%", 85);
log!(ERROR, "ファイルが見つかりません: {}", "data.txt");
}
出力形式:
[INFO] [src/main.rs:10] アプリケーション起動
[WARN] [src/main.rs:11] メモリ使用量: 85%
[ERROR] [src/main.rs:12] ファイルが見つかりません: data.txt
要件:
- ログレベル(INFO, WARN, ERROR, DEBUG)をサポート
- ファイル名と行番号を表示(
file!(),line!()を使用) - 可変長引数をサポート(
format!と同様)
2.3 struct_builder!マクロ(10点)
構造体とビルダーパターンを自動生成するマクロを実装してください。
macro_rules! struct_builder {
// ここに実装
}
struct_builder! {
User {
name: String,
age: u32,
email: Option<String>,
}
}
fn main() {
let user = User::builder()
.name("Alice".to_string())
.age(30)
.email(Some("alice@example.com".to_string()))
.build()
.unwrap();
assert_eq!(user.name, "Alice");
assert_eq!(user.age, 30);
}
要件:
- 構造体を定義する
builder()メソッドを持つ- 各フィールドに対応するセッターメソッド
build()メソッドでResultを返す- 必須フィールドが設定されていない場合はエラー
問題3:DSL設計(35点)
実用的なDSLを設計・実装しなさい。
3.1 ルーティングDSL(15点)
Webフレームワーク風のルーティングDSLを実装してください。
macro_rules! routes {
// ここに実装
}
routes! {
GET "/users" => list_users,
POST "/users" => create_user,
GET "/users/:id" => get_user,
DELETE "/users/:id" => delete_user,
}
fn list_users() -> String {
"ユーザー一覧".to_string()
}
fn create_user() -> String {
"ユーザー作成".to_string()
}
fn get_user() -> String {
"ユーザー取得".to_string()
}
fn delete_user() -> String {
"ユーザー削除".to_string()
}
fn main() {
// ルートを検索する関数が生成される
match find_route("GET", "/users") {
Some(handler) => println!("{}", handler()),
None => println!("Not Found"),
}
}
要件:
- HTTPメソッド(GET, POST, PUT, DELETE)をサポート
- パスとハンドラー関数のマッピング
find_route(method: &str, path: &str)関数を生成- マッチしたハンドラーを返す
3.2 テストケース生成DSL(10点)
パラメタライズドテストを生成するDSLを実装してください。
macro_rules! parameterized_test {
// ここに実装
}
fn add(a: i32, b: i32) -> i32 {
a + b
}
parameterized_test! {
add_tests for add:
case_1: (2, 3) => 5,
case_2: (-1, 1) => 0,
case_3: (0, 0) => 0,
case_4: (100, -50) => 50,
}
// これが展開される:
// #[test]
// fn add_tests_case_1() {
// assert_eq!(add(2, 3), 5);
// }
// #[test]
// fn add_tests_case_2() { ... }
// ...
要件:
- テスト関数名の自動生成
#[test]属性の付与assert_eq!による検証- 複数のテストケースをサポート
3.3 HTMLビルダーDSL(10点)
HTMLを生成するDSLを実装してください。
macro_rules! html {
// ここに実装
}
fn main() {
let page = html! {
<html>
<head>
<title>"My Website"</title>
</head>
<body>
<h1 class="header">"Welcome"</h1>
<p id="intro">"This is a paragraph."</p>
<ul>
<li>"Item 1"</li>
<li>"Item 2"</li>
<li>"Item 3"</li>
</ul>
<br />
</body>
</html>
};
println!("{}", page);
}
出力:
<html>
<head><title>My Website</title></head>
<body>
<h1 class="header">Welcome</h1>
<p id="intro">This is a paragraph.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<br/>
</body>
</html>
要件:
- タグのネスト
- 属性のサポート(
class,idなど) - 自己閉じタグ(
) - テキストノード
---
ボーナス課題
ボーナス1:手続き的マクロ入門(10点)
カスタム derive マクロを実装してください。
#[derive(Debug2)]マクロ
Debugトレイトと同様の機能を持つDebug2トレイトを実装する手続き的マクロを作成してください。
セットアップ:
# Cargo.toml
[package]
name = "debug2_derive"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
syn = "2.0"
quote = "1.0"
proc-macro2 = "1.0"
実装:
// lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Debug2)]
pub fn derive_debug2(input: TokenStream) -> TokenStream {
// ここに実装
}
使用例:
#[derive(Debug2)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 10, y: 20 };
println!("{:?}", p); // Point { x: 10, y: 20 }
}
要件:
- 構造体のフィールド名と値を出力
std::fmt::Debugトレイトを実装- 複数フィールドに対応
ボーナス2:JSONライクなDSL(10点)
JSON風の記法でRustのHashMapやVecを生成するDSLを実装してください。
macro_rules! json {
// ここに実装
}
fn main() {
let data = json!({
"name": "Alice",
"age": 30,
"hobbies": ["reading", "coding", "gaming"],
"address": {
"city": "Tokyo",
"zip": "100-0001"
}
});
// dataは serde_json::Value のような型になる
}
要件:
- オブジェクト(
{})のサポート - 配列(
[])のサポート - 文字列、数値のサポート
- ネストした構造のサポート
ボーナス3:状態機械DSL完成版(10点)
問題3で学んだ状態機械DSLを拡張し、以下の機能を追加してください:
macro_rules! state_machine {
// ここに実装
}
state_machine! {
name: TrafficLight,
initial: Red,
states: {
Red => {
on TimerExpired => Green {
println!("赤 → 緑");
},
},
Green => {
on TimerExpired => Yellow {
println!("緑 → 黄");
},
},
Yellow => {
on TimerExpired => Red {
println!("黄 → 赤");
},
},
}
}
fn main() {
let mut light = TrafficLight::new();
light.trigger(Event::TimerExpired); // "赤 → 緑"
light.trigger(Event::TimerExpired); // "緑 → 黄"
}
要件:
- 状態機械の名前を指定できる
- 各遷移に実行されるコードを指定できる
new()メソッドとtrigger()メソッドを生成- 不正な遷移を検出する
- コンパイル時間の測定
ボーナス4:マクロのパフォーマンス分析(10点)
マクロ展開のコストとランタイムパフォーマンスを分析してください。
タスク:
cargo build --timingsを使用- ランタイムパフォーマンス
cargo benchを使用- マクロ展開の確認
cargo expandで展開後のコードを確認
- 最適化されているか検証提出物:
- ベンチマークコード
- 分析レポート(1000文字以上)
- 最適化の提案
---
評価基準
マンダトリー部分(80点)
| 項目 | 配点 | 評価ポイント |
|---|---|---|
| 問題1:基本理解 | 15点 | マクロの概念の正確な理解 |
| 問題2:マクロ実装 | 30点 | 正確な実装と動作 |
| 問題3:DSL設計 | 35点 | 実用的で読みやすいDSL |
ボーナス部分(20点)
| 項目 | 配点 | 評価ポイント |
|---|---|---|
| ボーナス1:手続き的マクロ | 10点 | 正しい実装と動作 |
| ボーナス2:JSON DSL | 10点 | 完全な機能実装 |
| ボーナス3:状態機械拡張 | 10点 | 高度な機能の追加 |
| ボーナス4:パフォーマンス分析 | 10点 | 詳細な分析と考察 |
注: ボーナスは最大20点まで加算されます。
---
提出方法
ファイル構成
rust-foundations-19/
├── src/
│ ├── main.rs # 問題2, 3の実装
│ └── lib.rs # マクロ定義
├── answers.md # 問題1の回答
├── bonus1/ # ボーナス1
│ ├── Cargo.toml
│ └── src/lib.rs
├── bonus2/ # ボーナス2
│ └── src/main.rs
├── bonus3/ # ボーナス3
│ └── src/main.rs
└── bonus4/ # ボーナス4
├── benches/
│ └── macro_bench.rs
└── report.md
テスト実行
# マンダトリー
cargo test
# ボーナス1
cd bonus1
cargo test
# ボーナス4
cd bonus4
cargo bench
---
ヒント
問題2.1のヒント
再帰的アプローチ:
macro_rules! min {
// 1要素のベースケース
($x:expr) => { $x };
// 2要素の比較
($x:expr, $y:expr) => {
if $x < $y { $x } else { $y }
};
// 3要素以上:最初の2要素を比較し、残りと再帰
($x:expr, $y:expr, $($rest:expr),+) => {
// ヒント: min!(min!($x, $y), $($rest),+)
};
}
問題2.2のヒント
format!マクロの使い方:
macro_rules! log {
($level:ident, $msg:expr) => {
println!("[{}] [{}:{}] {}",
stringify!($level),
file!(),
line!(),
$msg);
};
($level:ident, $fmt:expr, $($arg:expr),+) => {
println!("[{}] [{}:{}] {}",
stringify!($level),
file!(),
line!(),
format!($fmt, $($arg),+));
};
}
問題3.1のヒント
ルーティングテーブルを生成:
type Handler = fn() -> String;
fn find_route(method: &str, path: &str) -> Option<Handler> {
let routes: Vec<(&str, &str, Handler)> = vec![
// マクロで生成
];
routes.iter()
.find(|(m, p, _)| *m == method && *p == path)
.map(|(_, _, h)| *h)
}
ボーナス1のヒント
synとquoteの基本的な使い方:
use syn::{Data, Fields};
#[proc_macro_derive(Debug2)]
pub fn derive_debug2(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
// 構造体のフィールドを取得
let fields = match input.data {
Data::Struct(ref data) => match data.fields {
Fields::Named(ref fields) => &fields.named,
_ => panic!("tuple structは未対応"),
},
_ => panic!("enumは未対応"),
};
// フィールド名を取得
let field_names: Vec<_> = fields.iter()
.map(|f| &f.ident)
.collect();
// コード生成
let expanded = quote! {
impl std::fmt::Debug for #name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct(stringify!(#name))
#(.field(stringify!(#field_names), &self.#field_names))*
.finish()
}
}
};
TokenStream::from(expanded)
}
---
学習の確認
この課題を通じて、以下を理解できたか確認してください:
macro_rules!の基本構文次の章では、Rustのテストシステムを学びます。