3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rustにおけるマクロと関数:どちらをいつ使うべきか?

Posted at

表紙

Rust の開発を行う際、よく「コードを簡潔にするためにマクロを使うべきか、それとも関数を使うべきか?」という疑問に直面します。

本記事では、マクロの使用シーンを分析し、どのような場面でマクロを活用すべきかを明確にします。まずは結論から述べましょう:

マクロと関数は互いに代替するものではなく、補完関係にあります。それぞれ得意な場面が異なり、適切に使い分けることで優れた Rust コードを書くことができます。

では、具体的にマクロの使用シーンを見ていきましょう。

マクロの分類

Rust のマクロには以下の種類があります:

  • 宣言的マクロ(Declarative Macros, macro_rules!
  • 手続き的マクロ(Procedural Macros)

手続き的マクロはさらに次の 3 つに分類されます:

  • カスタム派生マクロ(Custom Derive Macros)
  • 属性マクロ(Attribute Macros)
  • 関数風マクロ(Function-like Macros)

Rust において、関数とマクロはいずれもコードの再利用と抽象化に重要なツールです。関数は主にロジックをカプセル化し、既知の数や型の引数を処理し、型安全性と可読性を提供します。一方、マクロはコンパイル時にコードを生成し、関数では対応しきれないような場面(任意の数・型の引数の受け入れ、コード生成、メタプログラミングなど)に適しています。

具体的な使用シーンの分析

宣言的マクロ(macro_rules!

シナリオ ①:可変数の引数や異なる型の引数を処理する

問題点:

  • Rust の関数は定義時に引数の数と型を明示的に指定する必要があり、可変数の引数や異なる型の引数を直接受け取ることができません。
  • println! のような機能を実現したいが、任意の数・型の引数を受け入れる仕組みが必要。

マクロによる解決策:

  • 宣言的マクロはパターンマッチを利用して任意の数・型の引数を受け入れることが可能。
  • $()*$var などのメタ変数を使って、引数リストをキャプチャできる。

例:

// 可変長引数を受け取るマクロ
macro_rules! my_println {
    ($($arg:tt)*) => {
        println!($($arg)*);
    };
}

fn main() {
    my_println!("Hello, world!");
    my_println!("Number: {}", 42);
    my_println!("Multiple values: {}, {}, {}", 1, 2, 3);
}

関数の制限:

  • 関数は任意の数・型の引数を受け取るシグネチャを定義できない。
  • Rust の可変長引数を関数で扱うには format_args! などの特殊な仕組みが必要。

マクロと関数の協調:

  • マクロは引数の収集と展開を担当し、最終的に関数(println! の場合 std::io::stdout().write_fmt())を呼び出す。
  • 関数は実際のロジックの実行を担当し、マクロはコード生成や引数解析を行う。

シナリオ ②:重複するコードパターンの簡素化

問題点:

  • コード内に同じような処理が大量に存在する場合、手動で記述するとミスが発生しやすく、メンテナンスの負担も大きい。
  • 特に、テストケースの生成やフィールドアクセサの実装など、パターン化されたコードが頻出する。

マクロによる解決策:

  • 宣言的マクロを利用すると、パターンマッチを使って類似のコードを自動生成できる。
  • マクロを使えば、手作業によるコード記述を削減し、ミスを防ぐことが可能。

例:

// 構造体に対して getter メソッドを自動生成するマクロ
macro_rules! generate_getters {
    ($struct_name:ident, $($field:ident),*) => {
        impl $struct_name {
            $(
                pub fn $field(&self) -> &str {
                    &self.$field
                }
            )*
        }
    };
}

struct Person {
    name: String,
    email: String,
}

generate_getters!(Person, name, email);

fn main() {
    let person = Person {
        name: "Alice".to_string(),
        email: "alice@example.com".to_string(),
    };
    println!("Name: {}", person.name());
    println!("Email: {}", person.email());
}

関数の制限:

  • 関数では、入力に応じて複数の関数を動的に生成することはできず、各フィールドごとに手動で getter を実装する必要がある。
  • コンパイル時にコードを自動生成することは不可能で、メタプログラミング機能が不足している。

マクロと関数の協調:

  • マクロを使ってコード生成を行い、関数を具体的な実装として利用する。
  • 生成された関数を実際のコードで呼び出すことで、コードの可読性とメンテナンス性を向上させる。

シナリオ ③:小規模な埋め込み DSL(ドメイン特化言語)の実装

問題点:

  • より自然な構文やドメイン特化の記述を Rust のコード内で直接使用し、可読性や表現力を向上させたい。
  • Rust の標準的な文法では記述が冗長になりがちなケース(HTML や SQL のような構文を組み込みたい)。

マクロによる解決策:

  • 宣言的マクロを使うと、特定の構文を解析し、対応する Rust コードを生成できる。
  • パターンマッチと再帰的なマクロ展開を利用して、シンプルな DSL を構築可能。

例:

// 簡単な HTML DSL を定義するマクロ
macro_rules! html {
    ($tag:ident { $($inner:tt)* }) => {
        format!("<{tag}>{content}</{tag}>", tag=stringify!($tag), content=html!($($inner)*))
    };
    ($text:expr) => {
        $text.to_string()
    };
    ($($inner:tt)*) => {
        vec![$(html!($inner)),*].join("")
    };
}

fn main() {
    let page = html! {
        html {
            head {
                title { "My Page" }
            }
            body {
                h1 { "Welcome!" }
                p { "This is a simple HTML page." }
            }
        }
    };
    println!("{}", page);
}

関数の制限:

  • 関数では、カスタム構文の解析ができず、通常の Rust の式しか扱えない。
  • 関数のネストが深くなると、コードが冗長で直感的でなくなる。

マクロと関数の協調:

  • マクロはカスタム構文の解析を担当し、それを Rust コードに変換する。
  • 関数は文字列の整形や実行時処理を行う。

手続き的マクロ(Procedural Macros)

手続き的マクロは、Rust の抽象構文木(AST)を操作することで、より複雑なコード生成や変換を可能にする強力なマクロです。以下の 3 種類に分類されます:

  • カスタム派生マクロ(Custom Derive Macros)
  • 属性マクロ(Attribute Macros)
  • 関数風マクロ(Function-like Macros)

カスタム派生マクロ(Custom Derive Macros)

シナリオ:特定のトレイトを型に自動実装する

問題点:

  • Rust の型に対して同じトレイトの実装が必要な場合、手動で記述すると冗長でメンテナンスが面倒になる。
  • 型のフィールドや属性に応じて、実装内容をカスタマイズしたい。

マクロによる解決策:

  • カスタム派生マクロを利用すると、コンパイル時に型の定義を解析し、適切なコードを自動生成できる。
  • 例えば、serdeSerializeDeserialize などが典型的な例。

例:

// 必要なライブラリをインポート
use serde::{Serialize, Deserialize};

// #[derive] で Serialize / Deserialize を自動実装
#[derive(Serialize, Deserialize)]
struct Person {
    name: String,
    age: u8,
}

fn main() {
    let person = Person {
        name: "Alice".to_string(),
        age: 30,
    };

    // JSON 文字列にシリアライズ
    let json = serde_json::to_string(&person).unwrap();
    println!("Serialized: {}", json);

    // JSON からデシリアライズ
    let deserialized: Person = serde_json::from_str(&json).unwrap();
    println!("Deserialized: {} is {} years old.", deserialized.name, deserialized.age);
}

関数の制限:

  • 関数では、型の定義に応じてトレイトの実装を自動生成できない。
  • フィールドの情報に基づいたコード生成を行うことができない。

マクロと関数の協調:

  • カスタム派生マクロがトレイト実装のコードを生成し、関数はその実装を利用する。

属性マクロ(Attribute Macros)

シナリオ:関数や型の動作を修正する

問題点:

  • 関数や型の振る舞いをコンパイル時に変更したい(例:ログの自動追加、パフォーマンス測定、コードのインジェクション)。
  • 変更を簡潔な形で適用し、メンテナンスコストを下げたい。

マクロによる解決策:

  • 属性マクロを利用すると、関数や型にアノテーションを付けるだけで、新しい振る舞いを追加できる。
  • 例えば、実行前後にログを自動追加するような機能を実装可能。

例:

// 手続き的マクロを定義
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};

#[proc_macro_attribute]
pub fn log_execution(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let input = parse_macro_input!(item as ItemFn);
    let fn_name = &input.sig.ident;
    let block = &input.block;

    let expanded = quote! {
        fn #fn_name() {
            println!("Entering function {}", stringify!(#fn_name));
            #block
            println!("Exiting function {}", stringify!(#fn_name));
        }
    };

    TokenStream::from(expanded)
}

// マクロを関数に適用
#[log_execution]
fn my_function() {
    println!("Function body");
}

fn main() {
    my_function();
}

関数の制限:

  • 関数単体では、外部からの修正やコードのインジェクションができない。
  • ログを手動で追加すると冗長になり、ミスのリスクが増える。

マクロと関数の協調:

  • 属性マクロで関数の挙動を変更し、関数の本来のロジックはそのまま維持する。

関数風マクロ(Function-like Macros)

シナリオ:カスタム構文やコード生成を行う

問題点:

  • 特定の入力フォーマットを受け取り、対応するコードを生成したい(例:設定値の初期化、ルーティングの自動生成)。
  • 実行時ではなくコンパイル時にコードを処理したい。

マクロによる解決策:

  • 関数風マクロを利用すると、TokenStream を解析して適切な Rust コードを生成できる。
  • 特定のフォーマットの入力を受け取り、カスタム処理を行うのに適している。

例:

// 文字列を大文字に変換する関数風マクロ
use proc_macro::TokenStream;
use quote::quote;

#[proc_macro]
pub fn make_uppercase(input: TokenStream) -> TokenStream {
    let s = input.to_string();
    let uppercased = s.to_uppercase();
    let output = quote! {
        #uppercased
    };
    TokenStream::from(output)
}

// マクロの使用
fn main() {
    let s = make_uppercase!("hello, world!");
    println!("{}", s); // 出力: HELLO, WORLD!
}

関数の制限:

  • 関数ではコンパイル時に文字列を処理できず、実行時の変換が必要になる。
  • 実行時の変換は余分なコストが発生するため、コンパイル時に処理する方が効率的なケースがある。

マクロと関数の協調:

  • 関数風マクロはコンパイル時のコード生成を担当し、関数はその結果を利用してロジックを実行する。

どのように選択すべきか?

基本的な指針

  • 関数を優先: 関数で実装できる場合は、コードの可読性と保守性を優先して関数を使うべき。
  • マクロは適切に使う: 関数では対応できない場合のみマクロを活用するが、複雑になりすぎるとデバッグが難しくなるので注意。

関数が苦手でマクロが得意なシナリオ

  • 可変数の引数を受け付ける必要がある
  • コンパイル時のコード生成を行い、冗長性を削減する
  • カスタム構文(DSL)を作成し、直感的な表現を可能にする
  • トレイトの自動実装
  • コードの構造や動作をコンパイル時に変更する

マクロが苦手で関数が得意なシナリオ

  • 複雑なロジックの処理:関数の方が直感的で書きやすい
  • 型安全性とエラーチェック:関数は明示的な型情報を持ち、コンパイラの型チェックが適用される
  • 可読性と保守性:関数の方が展開後のコードが明確で理解しやすい
  • デバッグとテスト:関数はユニットテストやデバッグがしやすい

これらの指針を活用し、関数とマクロを適切に使い分けることで、より優れた Rust コードを実装できるようになります。


私たちはLeapcell、Rustプロジェクトのホスティングの最適解です。

Leapcell

Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:

複数言語サポート

  • Node.js、Python、Go、Rustで開発できます。

無制限のプロジェクトデプロイ

  • 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

比類のないコスト効率

  • 使用量に応じた支払い、アイドル時間は課金されません。
  • 例: $25で6.94Mリクエスト、平均応答時間60ms。

洗練された開発者体験

  • 直感的なUIで簡単に設定できます。
  • 完全自動化されたCI/CDパイプラインとGitOps統合。
  • 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

簡単なスケーラビリティと高パフォーマンス

  • 高い同時実行性を容易に処理するためのオートスケーリング。
  • ゼロ運用オーバーヘッド — 構築に集中できます。

ドキュメントで詳細を確認!

Try Leapcell

Xでフォローする:@LeapcellHQ


ブログでこの記事を読む

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?