1
0

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マクロ冬期講習Advent Calendar 2024

Day 7

Rustマクロ フラグメント指定子

Last updated at Posted at 2024-12-23

こちらの記事は Rustマクロ冬期講習アドベントカレンダー 7日目の記事です!

前回の記事で、「フラグメント指定子に慣れれば宣言マクロは制したようなもの」と言いました。

前回は以下に示すような表しか出さなかったので、今回はもう少し詳細を話していければと思います!

フラグメント指定子 説明
ident hoge, self 変数名や関数名などの識別子全般
expr 10 + 20, funcall(), if a { b } else { c } 式。いわゆる評価後に値を返す何か
literal 10, "hoge", true リテラル。言い換えると、ソースコード内で直接表現される固有値
stmt let a = 10;, struct Hoge;, fn foo() {} ステートメント。文。何かしら意味を持つものの評価値とはならないもの
ty i32, &'a str, fn() -> ()
path std::option::Option, Turbo::<Fish>, ::std::println パス。一見型と同じに見えるが、 :: で接続されたパスを表す意味が強い。例えば ::std::thread::spawn は関数であり型ではなく、これ自体はパスである
block {} スコープを作り出す中括弧を含めた全体
item pub fn foo() {}, use std::fs;, impl Hoge {} use文や関数宣言など。一部は文でもある。アイテム...という名前だとわかりにくいので、「ファイル内で最も外側に(あるいはトークン木の最上位に)配置できる要素」とおぼえておくと良さそう
pat / pat_param Some(ref hoge), None, 0..10 パターンマッチとして使用できる構造。簡単に言えば match 式のアーム部分になり得るもの。 pat_param は古いエディションとの互換性を保つためなので忘れて良い
lifetime 'a, 'static, '_ ライフタイム
meta derive(Debug), path = "hoge", serde 属性風マクロ等のアトリビュート(属性)、すなわち #[] の内側に書くことができる要素。めったに出てこないので忘れてよさそう
vis pub, pub(crate) Visibility、すなわち可視性の略。これのみをマッチさせる必要もないためめったに出てこない
tt トークン木 (Token Tree) のことで、 任意の Rustコード片の塊にマッチするワイルドカード。つまりジョーカー

「フラグメント指定子がどうしても覚えられない!!!!」という筆者のような方向けに、フラグメント指定子判別器を作ったので良かったら使ってみてください!

Rustマクロ フラグメント指定子(ident, expr, item, stmt...)なんもわからん!となったので判別器作った #Rust - Qiita

フラグメント指定子についてどうまとめようか迷っていたのですが、下手にひねってもアレなので今回は素直に記事作成の参考にしている The Little Book of Rust Macros の内容及び掲載例を拝借しつつ、紹介したい順で紹介することにします!

tt: トークン木

これだけは特殊なフラグメント指定子なので最初に持ってきました。トークン木は(ほぼ) 任意の構文要素 にマッチします。いわばワイルドカードで、トランプで言えばジョーカー、ウノで言えばドロフォー、ポ◯モンで言えばメタ◯ンみたいなやつです。

「なんでトークン木って名前なの?」とか「トークン木ってなんぞ?」って方はRustマクロ冬期講習アドベントカレンダー2日目の記事 Rustマクロの事前知識①「入出力はトークン木」 #Rust - Qiita を参考にしていただけると幸いです。

活用事例としては再帰呼び出しを利用しているマクロにおいて残り部分を渡す、などです。Little Bookの例が一番わかりやすいですね。

Rust
macro_rules! mixed_rules {
    () => {};
    (trace $name:ident; $($tail:tt)*) => {
        {
            println!(concat!(stringify!($name), " = {:?}"), $name);
            mixed_rules!($($tail)*);
        }
    };
    (trace $name:ident = $init:expr; $($tail:tt)*) => {
        {
            let $name = $init;
            println!(concat!(stringify!($name), " = {:?}"), $name);
            mixed_rules!($($tail)*);
        }
    };
}

fn main() {
    let a = 42;
    let b = "Ho-dee-oh-di-oh-di-oh!";
    let c = (false, 2, 'c');
    mixed_rules!(
        trace a;
        trace b;
        trace c;
        trace b = "They took her where they put the crazies.";
        trace b;
    );
}

(引用元: https://veykril.github.io/tlborm/decl-macros/patterns/tt-muncher.html)

利用頻度は低いのですがとりあえず「他とは異なる」ことを強調したいため最初に持ってきました。何にでもマッチしますがおそらくフラグメント指定子として最初に出てくる候補ではありません。

識別子 ident

identifier つまり識別子 (変数名や関数名など) またはキーワードにヒットします。

Rust
macro_rules! idents {
    ($($ident:ident)*) => ();
}

idents! {
    // _ <- This is not an ident, it is a pattern
    foo
    async
    O_________O
    _____O_____
}

名前っぽいやつを渡したい時はだいたいこいつです!

expr

expression、すなわち「式」にヒットします。Rustは式指向言語なので、if "式" ( if a { b } else { c } ) などヒットするものが意外と多いです。

Rust
macro_rules! expressions {
    ($($expr:expr)*) => ();
}

expressions! {
    "literal"
    funcall()
    future.await
    break 'foo bar
}

リテラル literal

文字列 "aaa" 、数値 10、 真偽値 true など、いわゆる言語組み込みの値にマッチします。

Rust
macro_rules! literals {
    ($($literal:literal)*) => ();
}

literals! {
    -1
    "hello world"
    2.3
    b'b'
    true
}

stmt

文、すなわちプログラムとして意味はあるものの式とは違って値を返さないとされるもの( () を返してるとも受け取れる...?)にヒットします。

Rust
macro_rules! statements {
    ($($stmt:stmt)*) => ($($stmt)*);
}

fn main() {
    statements! {
        struct Foo;
        fn foo() {}
        let zig = 3
        let zig = 3;
        3
        3;
        if true {} else {}
        {}
    }
}

今回のLittle Bookが掲載している例は興味深いのでちょっと掘り下げようと思います。

マクロが展開された結果(もとい、文としてキャプチャして文として出力し直した結果)はこうなるそうです。

Rust
/* snip */

fn main() {
    struct Foo;
    fn foo() { }
    let zig = 3;
    let zig = 3;
    ;
    3;
    3;
    ;
    if true { } else { }
    { }
}

セミコロンの数が何やら不自然ですね。一つ一つ見てみましょう!

  • struct Foo; : ユニット型系構造体はセミコロンなし( struct Foo )では文にならないので、 struct Foo; まででマッチしてこれがそのまま出力される
  • fn foo() {} : これで文になっているのでそのまま出力される
  • let zig = 3 : これで文になっているのでキャプチャされ、出力時に セミコロンが付与されている
  • let zig = 3; : セミコロンより前の時点で文としてマッチして、 セミコロンが付与される ので結果としてここでセミコロンが増えている
  • 3 : リテラルによる式は文として扱われてマッチするので、 セミコロンが付与される 。2回目で上記の let zig = 3 と同様に余計なセミコロンが増えている
  • if true {} else {}, {} : 同じ式でも、ブロックにより区切られるためセミコロンを必要としないものたちにはセミコロンは付与されない

ty

型にマッチします。 std::option::Option みたいな普通のものだけでなく、 &'static dyn strfn() -> () (関数ポインタ)みたいな型もヒットします。

Rust
macro_rules! types {
    ($($type:ty)*) => ();
}

types! {
    foo::bar
    bool
    [u8]
    impl IntoIterator<Item = u32>
}

パス path

型と似ているけども、use 文などで使用する :: でつなげられた識別子の集まりがヒットします。なぜか Fn() -> () 系のやつもヒットするらしい。謎

Rust
macro_rules! paths {
    ($($path:path)*) => ();
}

paths! {
    ASimplePath
    ::A::B::C::D
    G::<eneri>::C
    FnMut(u32) -> ()
}

ブロック block

中括弧で区切られスコープを作り、評価されると最後の式の値になる { } <- こいつのことです!

Rust
macro_rules! blocks {
    ($($block:block)*) => ();
}

blocks! {
    {}
    {
        let zig;
    }
    { 2 }
}

アイテム item

構造体や列挙体、関数、定数の定義を行う部分にヒットします。

Rust
macro_rules! items {
    ($($item:item)*) => ();
}

items! {
    struct Foo;
    enum Bar {
        Baz
    }
    impl Foo {}
    pub use crate::foo;
    /*...*/
}

定義類は文でもありがちなので文との違いがわかりにくいですが、アイテムは「 fn main() {} の外側に書くことができる構文要素」と思っておくと覚えやすいです!

パターン pat / pat_param

パターンは match アームの => の左側にある部分のことを指します。実は match 式だけではなく let 文や if let 文、関数の引数部分など、 match アーム以外にもパターンを活用できるシーンは多々あったりします。

Rust
macro_rules! patterns {
    ($($pat:pat)*) => ();
}

patterns! {
    "literal"
    _
    0..5
    ref mut PatternsAreNice
    0 | 1 | 2 | 3
}

旧エディションとの互換性のために pat_param というものも設けられていますが気にしなくて大丈夫です。

ライフタイム lifetime

ライフタイムにヒットします。

Rust
macro_rules! lifetimes {
    ($($lifetime:lifetime)*) => ();
}

lifetimes! {
    'static
    'shiv
    '_
}

メタ meta

一番馴染みが薄い構文要素かもしれません。属性風マクロ #[...]... の部分がメタです。というわけで良く書くメタは例えば derive(Debug, Clone, Copy) とか。

Rust
macro_rules! metas {
    ($($meta:meta)*) => ();
}

metas! {
    ASimplePath
    super::man
    path = "home"
    foo(bar)
}

docコメントの正体

Little Bookの方で差し込まれていた豆知識だったのでこちらでも書くと、

Rust
struct Hoge {
    /// ドキュメントコメント
    field: u32,
}

みたいにつける /// で始まるドキュメントコメントは、実は属性 #[doc = "..."] の糖衣構文だったりします!

だから通常のコメントと違ってコンパイラに解析されエラーになることがあるのです。

可視性 vis

pubpub(crate) といった、アイテムが他のモジュールから見えるかを指定する可視性にヒットします。

Rust
macro_rules! visibilities {
    //         ∨~~Note this comma, since we cannot repeat a `vis` fragment on its own
    //        (visの後にカンマを直接つなげることはできないので、メタ変数側に取り込んで無理やりトークンを消費する)
    ($($vis:vis,)*) => ();
}

visibilities! {
    , // no vis is fine, due to the implicit `?`
    pub,
    pub(crate),
    pub(in super),
    pub(in some_path),
}

本稿では説明を省略しますが、「無」にもマッチする珍しい指定子なのでマクロでもおもしろ挙動をするようです。ちなみに何も指定していない時の可視性は pub(self) と等しく、プライベートなアイテムになっています。

ref: https://veykril.github.io/tlborm/decl-macros/minutiae/fragment-specifiers.html#vis

まとめ・所感

うん!やっぱり多い!フラグメント指定子を使用頻度順に解説しましたがここまで真面目に全部の解説を読んだ人なんていないでしょう。え、読んだ?ありがとうございます。

何はともあれ、慣れてきたら ident とか expr ばかりになるでしょうし、わからなくなったら筆者が作ったツールを使えば対応する指定子がすぐわかるので(しつこいステマ)、忘れた時に調べ直すことはあってもまともに覚えている必要はないでしょう。

このフラグメント指定子についてさえ慣れれば、宣言マクロもそしてこれからやる手続きマクロもスラスラ書けるようになります!ぜひ慣れていきましょう!

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?