6
5

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で書く! 世界一分かりやすいマクロ入門

Last updated at Posted at 2023-12-23

はじめに

本記事はRustのproc_macroを用いて、自由自在にマクロを操れるようになろうというものです。つい1ヶ月前まで私も「マクロ?なにそれ美味しいの?」状態だったので全く難しいものでもありません。この記事を通してマクロを書けるようになると皆様のRustコード全体の見通しが良くなり、眩しいプログラミングライフを手に入れられることでしょう!!

読者対象

  • Rustに興味があるけどなかなか手を出せてない方
  • ある程度の手続き的なプログラミングの経験がある方
  • そもそもマクロってなんなのか分からない方
  • etc...

そもそもマクロとは

簡単にいうと「プログラムを生成するプログラム」です。
「???」となる方も少なくないと思います。簡単な例を見てみましょう。


新入社員のヒヨコのピヨ子さん(🐣)は上司(🐔)から10000個の要素を持つEnumを明日までに定義してねと頼まれました。

10000個のバカデカEnum.rs
enum Sample {
    Value1,
    Value2,
    ...
}

5時間経ちましたがピヨ子さんは300個を超えたあたりで腱鞘炎になり退勤...

ピヨ子さんはどうすればよかったのでしょうか。1から10000までのEnumを手作業で生成していたら日が暮れてしまうのは明白でしょう。
そこで出てくるのが「マクロ」です!!

10000個のバカデカEnum.rs
bakadeka_10000_enum!();

fn main() {
    let value1 = Sample::Value1;
    let value9567 = Sample::Value9567;
}

雰囲気だけ掴んで頂きたいのでここでは内部実装を割愛しますが、こんな感じにEnumを定義できちゃいます。
「バカデカEnumなんて需要ねぇんだよ!」と思ってる方もいらっしゃるかもしれませんが、マクロでできる0.01%を紹介したまでです。怒らないでください。

事前学習

注意

proc_macroはライブラリクレート内でのみ定義可能です。
それ以外の場所で定義するとコンパイラに怒られるので新しくプロジェクトファイルを作成して下さい。

必要になるcrateと設定

proc_macroを定義するクレートのcargo.tomlではproc-macro = trueとする必要があります。
また構文木を扱いやすくするための便利なクレート(syn, proc-macro2, quote)があるのでdependenciesに追加して下さい。
anyhowは好みで構いません。

cargo.toml
[package]
name = "macros"
version = "0.1.0"
edition = "2021"
resolver = "2"

[dependencies]
proc-macro2 = "1.0.69"
syn = "2.0.38"
quote = "1.0.33"
anyhow = "1.0.75"

[lib]
proc-macro = true

実際になにか作ってみよう

百聞は一見にしかず。作りながら一緒に覚えていきましょう。

作るもの

競技プログラミングの標準入力を簡単にできるようにするマクロを作ってみましょう。
次の様な入力があったとします。

入力
hello
1
1 2 3 4 5
1 2 3
4 5 6
7 8 9
1 Hello
2 Hi
1 Hello 2 3
4 Wow 5 6
1 2

普通に書くのであればfor文をぶん回したりして面倒臭いですが、次のように受け取れたら楽ですよね。

main.rs
fn main() {
    include_input! {
        hello: String,
        one: i128,
        array: [u8; 5],
        array_array: [[i32; 3]; 3],
        array_tuple1: [(u16, String); 2],
        array_tuple2: [(u16, String, usize, usize); 2],
        tuple: (u8, u8),
    };

    println!(
        "\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}",
        hello, one, array, array_array, array_tuple1, array_tuple2, tuple
    );
}

今回はこれを作ってみましょう。

入力の受け取り

include_input! {
    hello: String,
    one: i128,
    array: [u8; 5],
    array_array: [[i32; 3]; 3],
    array_tuple1: [(u16, String); 2],
    array_tuple2: [(u16, String, usize, usize); 2],
    tuple: (u8, u8),
};

フィールド名: 型,の形が連続していることが分かります。まずはこれを受け取れるようにしましょう。
「面倒な書き方で定義するのかな?」って思ったでしょ。めっちゃ簡単に定義できます。

まずフィールド名: 型から定義してみます。

name_and_type.rs
use proc_macro2::Ident;
use syn::parse::{Parse, ParseStream};
use syn::{Token, Type};

pub struct NameAndType {
    name: Ident,
    _colon: Token![:],
    ty: Type,
}

impl Parse for NameAndType {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let name = input.parse()?;
        let _colon = input.parse()?;
        let ty = input.parse()?;
        Ok(NameAndType { name, _colon, ty })
    }
}

あれ、めちゃめちゃ簡単ですね。

  • name: Ident
    • ここはフィールド名など一意なデータを指すものにマッチします。Identは他にも変数名や構造体名などにもマッチするのでderiveマクロを書く時にもお世話になります。
  • _colon: Token![:]
    • ここは見ての通り:にマッチします。実際には_colonを使ってなにかをするわけではないのですが、マッチ条件の可読性を上げるために公式が書くことを推奨しています。意地でも書きたくなければスキップしても良いでしょう。
  • ty: Type
    • ここは型名にマッチします。u8, String, [u16; 5]など、型であればマッチします。

これで入力の第1ステップは完了です。
続いてNameAndType,の繰り返しにマッチするようにしましょう。
繰り返しに関してはsyn::punctuated::Punctuated<T, P>が役に立ちます。

input_punctuated.rs
use crate::name_and_type::NameAndType;
use std::ops::Deref;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::Token;

pub struct MyPunctuated(Punctuated<NameAndType, Token![,]>);

impl Parse for MyPunctuated {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        Ok(MyPunctuated(Punctuated::parse_terminated(input)?))
    }
}

impl Deref for MyPunctuated {
    type Target = Punctuated<NameAndType, Token![,]>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

このように簡単に定義できてしまいました。 Derefはお好みで追加して下さい。
あとはマクロ本体を定義するだけで入力までは動くようになります。

lib.rs
use input_punctuated::MyPunctuated;
use proc_macro::TokenStream;
use syn::parse_macro_input;

#[proc_macro]
pub fn include_input(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as MyPunctuated);
    TokenStream::new()
}

マクロを定義するときは#[proc_macro]と関数の頭に書くだけでinclude_input!(...)のように呼び出すことが可能になります。
引数と戻り値にTokenStreamとありますがこれは構文木と言って、プログラムを木構造として保持しているものになります。
inputを先ほど定義したMyPunctuatedに変換して、戻り値は一旦空のTokenStreamを返していざ実行してみましょう!

main.rs
fn main() {
    include_input! {
        hello: String,
        one: i128,
        array: [u8; 5],
        array_array: [[i32; 3]; 3],
        array_tuple1: [(u16, String); 2],
        array_tuple2: [(u16, String, usize, usize); 2],
        tuple: (u8, u8)
    }
}

おそらく何も起こらずに終了したかと思います。しかし内部では全てのフィールド名: 型,はしっかりMyPunctuatedに変換されています。
これで晴れて入力は完全にマスターしましたね!

プログラムを自動生成してみる

expand_input.rs
use proc_macro2::TokenStream;
use quote::quote;

pub fn expand_input(input: MyPunctuated) -> TokenStream {
    quote! {
        println!("Hello, World!");
    }
}

新しくファイルを作ってMyPunctuatedからTokenStreamを生成する関数を定義しましょう。
ここで注意が必要なのが、ここで使うTokenStreamproc_macro2::TokenStreamです。lib.rsTokenStreamproc_macro::TokenStreamなので気をつけましょう。

ではlib.rsexpand_inputをimportして書き直してみましょう。

lib.rs
use crate::input::expand_input;
use input_punctuated::MyPunctuated;
use proc_macro::TokenStream;
use syn::parse_macro_input;

#[proc_macro]
pub fn include_input(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as MyPunctuated);
    expand_input(input).into()
}

proc_macro2::TokenStreamからproc_macro::TokenStreamに変換するには、Into traitが実装されているので末尾に.into()を書くだけで良いです。

実際に動かしてみてください!なにもないところからHello, World!が現れたと思います。これは何を意味しているかというと、入力の変換に成功して尚且つプログラムの自動生成に成功したことを意味しています。

MyPunctuatedからプログラムを生成する

MyPunctuated,で区切られたNameAndTypeをもとに生成しているので直感的にMyPunctuatedはiteratorだと勘付くかもしれません。
実はその通りで、Derefを実装していればMyPunctuatedからiter()を呼べて、個々のNameAndTypeにアクセスできます。これをもとにプログラムを自動生成するのです。

input.rs
pub fn expand_input(input: MyPunctuated) -> anyhow::Result<TokenStream> {
    let token_streams = input
        .iter()
        .map(|field| (field.name(), field.ty()))
        .map(|(ident, ty)| expand_several_type(ident, ty, 0))
        .collect::<Vec<_>>();
    // Vec<Option<TokenStream>> => Vec<TokenStream> に変換する
    let mut result = Vec::new();
    for token_stream_result in token_streams {
        result.push(token_stream_result?);
    }
    Ok(quote! {
        #(#result)*
    })
}

fn expand_several_type(ident: &Ident, ty: &Type) -> anyhow::Result<TokenStream> {
    todo!()
}

IdentTypeからプログラムを生成できるようにexpand_several_type(ident: &Ident, ty: &Type)を定義しましょう!expand_several_typeは入力に依存する部分なのでanyhowを用いて失敗可能性を表現しておきましょう。
(panicでも別に良いですが、Resultからcompile_errorに変換することが可能なのでpanicで逃げる必要性は薄いと言って良いと思ってます)

ここで以下のような表現にびっくりしているかもしれません。

quote! {
    #(#result)*
}

実はquoteマクロの中では#を用いて変数を展開することが可能なのです!
(厳密にはToTokens traitを実装している型であれば可能)

文字列型や数字型はもちろんのことながら、Vec<TokenStream>にも対応していて表現の幅が広いです。
Identも展開可能なのでIdentを生成してやれば任意の文字を展開できます。
Vec<TokenStream>に対しては#(#token_streams),*のようにすると?,?,?,...のように展開させることが可能です。

example.rs
let number = 1;
let string = String::from("hello");
let ident = Ident::new(&format!("{}_{}", string, number), Span::call_site());
let token_streams = vec![quote!(10), quote!("Wow"), quote!("Yay")];
quote! {
    let hoge = #number; // let hoge = 1;       として展開される
    let fuga = #string; // let fuga = "hello"; として展開される
    let #ident = 1u8;   // let hello_1 = 1u8;  として展開される
    let tuple = (#(#token_streams),*); // let tuple = (10, "Wow", "Yay"); として展開される
}

コレが分かれば、あとはもうパズルのように組み立てるだけですね!
実際に組み立ててみたのが以下の通りです。

input.rs
/// Typeに合わせて展開する
fn expand_several_type(ident: &Ident, ty: &Type, depth: i8) -> anyhow::Result<TokenStream> {
    // サポートするのは2次元までにする
    if depth >= 3 {
        bail!("Maximum depth reached");
    }
    match ty {
        Type::Array(type_array) => expand_array(ident, type_array.clone(), depth),
        Type::Tuple(type_tuple) => Ok(expand_tuple(ident, type_tuple.clone(), depth)),
        Type::Path(_) => Ok(quote! {
            let mut #ident = String::new();
            ::std::io::stdin().read_line(&mut #ident).expect("failed to read");
            let #ident = #ident.trim().to_string().parse::<#ty>().unwrap();
        }),
        _ => bail!("Unsupported type"),
    }
}

/// タプルとして展開する
fn expand_tuple(ident: &Ident, type_tuple: TypeTuple, depth: i8) -> TokenStream {
    let token_streams = type_tuple
        .elems
        .iter()
        .enumerate()
        .map(|(i, ty)| {
            quote! {
                split_input[#i].parse::<#ty>().unwrap()
            }
        })
        .collect::<Vec<_>>();
    let token_stream = quote! {
        let mut input = String::new();
        ::std::io::stdin().read_line(&mut input).expect("failed to read");
        let trimed_string = input.trim().to_string();
        let split_input = trimed_string.split(' ').collect::<Vec<_>>();
    };
    if depth == 1 {
        quote! {
            #token_stream
            let #ident = (#(#token_streams),*);
        }
    } else {
        quote! {
            #token_stream
            #ident.push((#(#token_streams),*));
        }
    }
}

/// 配列として展開する
fn expand_array(ident: &Ident, type_array: TypeArray, depth: i8) -> anyhow::Result<TokenStream> {
    let (array_element_type, array_length) = (type_array.elem, type_array.len);
    match *array_element_type {
        Type::Array(_) | Type::Tuple(_) => {
            let token_stream = expand_several_type(ident, &array_element_type, depth + 1)?;
            Ok(quote! {
                let mut #ident = Vec::new();
                for _ in 0..#array_length {
                    #token_stream
                }
                let #ident: [#array_element_type; #array_length] = #ident.try_into().expect("Failed to cast to an array from Vec");
            })
        }
        Type::Path(_) => {
            let token_stream = quote! {
                .trim()
                .split(" ")
                .map(|n| n.parse::<#array_element_type>().expect("Failed to cast"))
                .collect::<Vec<_>>()
                .try_into()
                .expect("Failed to cast to an array from Vec");
            };
            let result = if depth == 1 {
                quote! {
                    let mut #ident = String::new();
                    ::std::io::stdin().read_line(&mut #ident).expect("Failed to read");
                    let #ident: [#array_element_type; #array_length] = #ident #token_stream
                }
            } else {
                quote! {
                    let mut input = String::new();
                    ::std::io::stdin().read_line(&mut input).expect("Failed to read");
                    let input: [#array_element_type; #array_length] = input #token_stream
                    #ident.push(input);
                }
            };
            Ok(result)
        }
        _ => {
            bail!("Unsupported type");
        }
    }
}

複雑に見えますが、ゴールを想像するところから始めればとても簡単です。
おそらく最初は知らない型が多くあると思いますが、tuplestructが何を持っているか(識別子・Visility・型など)を頭の片隅に置いておくと大体何を指しているのか分かるはずです。

仕上げはResult型にしたのでErrの場合はコンパイルエラーにしてあげましょう。

lib.rs
#[proc_macro]
pub fn include_input(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as MyPunctuated);
    expand_input(input)
        .map_err(|e| syn::Error::new(proc_macro2::Span::call_site(), e.to_string()))
        .unwrap_or_else(|e| e.into_compile_error())
        .into()
}

map_errを用いてsynErrorに変換してあげるだけでOKです。
完成しましたね! お疲れ様でした。

実行してみよう🎉

入力

-> cargo run
hello
1
1 2 3 4 5
1 2 3
4 5 6
7 8 9
1 Hello
2 Hi
1 Hello 2 3
4 Wow 5 6
1 2

実行結果

"hello"
1
[1, 2, 3, 4, 5]
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
[(1, "Hello"), (2, "Hi")]
[(1, "Hello", 2, 3), (4, "Wow", 5, 6)]
(1, 2)

しっかり型変換もできていますね!
タプルや配列もしっかり生成されています。

まとめ

quoteマクロを使えば簡単にさまざま表現ができることがわかりました。
マクロと聞いて抵抗感があった人でも少しは心理的ハードルが下がったのではないでしょうか。
まだまだ他にも紹介しきれていないものもありますが、今回は入門記事ということでここらへんでやめておこうと思います。

6
5
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
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?