課題19: マクロとDSL作成

マンダトリー要件

問題1:基本的なマクロ理解(15点)

以下の質問に答えなさい。

  • マクロと関数の違い(5点)
- マクロと関数の3つの主要な違いを説明しなさい。 - なぜ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のHashMapVecを生成する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のヒント

    synquoteの基本的な使い方:

    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!の基本構文
  • [ ] メタ変数と繰り返しパターン
  • [ ] マクロのデバッグ方法
  • [ ] 実用的なDSLの設計
  • [ ] 手続き的マクロの基礎

次の章では、Rustのテストシステムを学びます。