239
172

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rustのマクロを覚える

Last updated at Posted at 2016-12-20

はじめに

Rust のドキュメントを読んだり実際に動かしたりして理解したことをまとめていたら1、マクロの説明だけでもかなり長くなりそうなので、1つの記事にしてみました。

マクロには以下のデメリットがあるため、一般的にはあまり利用しない方がいいと言われます。

  • コードが理解しづらい
  • 良いマクロを書くのは難しい
  • コンパイルエラーは展開後のコードで起こるので、原因が分かり難い

C/C++ のマクロはコンパイラとは別に用意されたプリプロセッサにより、構文を解釈しない単純な文字列置換が行われます。そのため注意して利用しないと意図しない動作を起こすことがあります2。一方、Rust のマクロはそのような問題が起こらない仕組みになっています。

なお「“本物のマクロ”でCのコード行数を半分に!」という記事によれば、「Programming Clojure」(Stuart Halloway著)に以下の警告が書いてあるそうです。

ルール1:マクロは書くな
ルール2:それがパターンをカプセル化する唯一の方法ならば、マクロを書け
例外:同等の関数に比べて、呼び出し側が楽になるならば、マクロを書いても構わない

Rust も可能なら関数、トレイト、ジェネリクスなどのマクロ以外の言語機能を使うことを考えるべきでしょう。しかしマクロでないと似たようなコードの繰り返しを共通化できない場合もあり、うまく使えばとても有用なものでもあります。

Rust はドキュメントが充実していますし、日本語訳もあります。あえて記事にするからには、「使えるようになること」に主眼をおいて、公式よりも実用的な説明を目指してみます。細かい仕様は Rust 公式の説明を参照してください。

動作確認は Rust 1.13 で行っています。以下、説明を簡潔にするため丁寧語をやめます。

マクロの基本形式

マクロは以下の形式になっている。 "=>" の左側をマッチャーと呼び、ここにマッチしたものが右側に展開される。左右とも () または {} または [] で囲む。どれで囲んでも一緒。

macro_rules! foo {
    () => ()
}

foo!();  // 何もしない

呼び出し時の括弧

マクロ定義と同様に中身を囲むのに (), {}, [] が使える。どれで囲んでも動作は一緒。マクロ定義と揃える必要もない。

foo!();  // () で囲むのは勿論OK。関数呼び出しっぽく使う場合はこれ。
foo![];  // [] で囲んでもいい。vec! とかはこの方が配列っぽくて分かりやすい。
foo!{}   // {} で囲んでもいい。ブロックっぽく使う場合はこれ。セミコロンで終わらなくてもいい。

マッチャーに使う指定子(disanator)

$name:disanator の形式で、何にマッチさせるか(disanator)と、マッチさせたものを格納するメタ変数 ($name)を指定できる。以下の expr は Rust の式にマッチする。

macro_rules! foo {
    ($x:expr) => { println!("{}", $x); }
}

foo!(1 + 2);  // println!("{}", 1 + 2);

disanator には以下が利用できる。それぞれについて詳細は後述する。

指定子 マッチするもの
item アイテム(関数、構造体など)
block ブロック
stmt ステートメント(文)
pat パターン
expr
ty
ident 識別子
path 修飾された名前。例: T::SpecialA
tt 単一のトークン木
meta アトリビュートの中身。例: cfg(target_os = "windows")

item

Rust におけるアイテムは当てはまるものが広すぎて分かり難い。アイテムとはクレートを構成する部品であり、モジュールのネストによって組織化され、パスを持つ・・・んだそうですよ。それでアイテムの例は以下だそうで。

  • extern crate 宣言
  • use 宣言
  • モジュール
  • 関数
  • 型定義
  • 構造体 (struct)
  • 列挙子 (enum)
  • 定数 (const)
  • 静的アイテム (static)
  • トレイト (trait)
  • トレイトの実装 (impl ... for ... { ... })

どうもモジュール内に直接置けるものをアイテムと呼んでいるっぽい。

// item にマッチしたらその内容を表示する。stringify! マクロは渡されたものを文字列化する。
macro_rules! match_item {
    ( $i:item ) => (println!("item: {}", stringify!($i)))
}

match_item!(fn do_something() { });
match_item!(mod hoge { });
match_item!(struct Hoge {
        value: i8,
        text: String,
    });

block

{ } で囲まれるブロックにマッチする。

macro_rules! match_block {
    ( $b:block ) => (println!("block: {}", stringify!($b)))
}

match_block!({ });
match_block!({ 10 });
match_block!({
        let x = 10;
        println!({}, x);
    });

stmt

文にマッチする。Rust では式に ; を付けた式文と、let を使った宣言文だけ・・・って思ってたんだけど・・・式や宣言以外にも、ブロック、アイテム、識別子、パスも全部マッチしてしまうみたい。

macro_rules! match_stmt {
    ( $s:stmt ) => (println!("stmt: {}", stringify!($s)))
}

match_stmt!(let a = 10);
match_stmt!(10);
match_stmt!(println!("{}", 5));
match_stmt!({ 10 });
match_stmt!({
        let x = 10;
        println!({}, x);
    });
match_stmt!(struct Hoge {
        value: i8,
        text: String,
    });
match_stmt!(macro_rules! test { () => () });
match_stmt!(fn do_something() { });
match_stmt!(mod hoge { });
match_stmt!(MyType);
match_stmt!(std::str);

ただし Rust の構文に合っていなければ当然エラーになる。

// これは Rust の文にはなりえないのでエラーになる
match_stmt!(int a = 10);  // error: no rules expected the token `a`

あと、; を付けてもエラーになる。え??

// ; を含めてはいけないらしい
match_stmt!(let a = 10;);  // error: no rules expected the token `;`

pat

match などで使われるパターンにマッチする。

macro_rules! match_pat {
    ( $p:pat ) => (println!("pat: {}", stringify!($p)))
};

match_pat!(1);
match_pat!(_);
match_pat!(2...9);
match_pat!(e @ 1 ... 5);
match_pat!(None);
match_pat!(Some(x));

複数パターンを | で繋げられるが、繋げたものにはマッチしない。

// 以下はエラー。| の左右の個別のパターンにはマッチするが、繋げた全体にはマッチしない。
match_pat!(0 | 1); // error: no rules expected the token `|`

match のパターンは if で条件指定できるが、条件指定部分にはマッチしない。

// 以下はエラー。if の前のパターンにはマッチするが、if を含む全体にはマッチしない。
match_pat!(Some(x) if x < 10);  // error: no rules expected the token `if`

expr

式にマッチする。Rust は式ベース言語であり、ブロックや if などの制御構文も式なので、C/C++ より多くのものが式として認識される。

macro_rules! match_expr {
    ( $e:expr ) => (println!("expr: {}", stringify!($e)))
};

match_expr!(10);
match_expr!(10 + 2 * 100 );
match_expr!({
    let result = do_something();
    report_result(result);
    result
});
match_expr!(if initialized { start() } else { report_error() });
match_expr!(if initialized { start() }); // else がないのに・・・

上記で else がない if も式として認識している。マクロはコンパイル前に展開されるため、start() の戻り値の型を認識しておらず、もし戻り値の型が () だったらこれは正しい式になるためかと。

もちろん let による束縛は式ではなく文なので、expr ではマッチしない。

// 以下はエラー
match_expr!(let a = 10);  // error: expected expression, found statement (`let`)

ty

型にマッチする。マクロの展開はコンパイル前に行われるので、既知の型でなくてもいい。

macro_rules! match_ty {
    ( $t:ty ) => (println!("ty: {}", stringify!($t)))
};

match_ty!(i32);
match_ty!(String);
match_ty!(&[i32]);
match_ty!(UnknownType);  // 未知の型
match_ty!(html);  // 頭小文字の未知の型
match_ty!(std::str);  // パス付き

未知の型はOKだけど、match, if, fn などの予約語は受け付けない。

// 予約語はエラー
match_ty!(match);  // error: expected type, found keyword `match`

ident

識別子にマッチする。こちらは ty と違って予約語でもマッチしてしまう。

macro_rules! match_ident {
    ( $i:ident ) => (println!("ident: {}", stringify!($i)))
};

match_ident!(i32);
match_ident!(String);
match_ident!(UnknownType);
match_ident!(match);
match_ident!(if);
match_ident!(fn);

path

modname::typename のような形式のパスにマッチする。

macro_rules! match_path {
    ( $p:path ) => (println!("path: {}", stringify!($p)))
};

match_path!(i32);
match_path!(::main);
match_path!(std::str);

tt

Rust 公式の説明日本語訳)には a single token tree にマッチするとある。なお Rust のリファレンス には either side of the => in macro rules(マクロ内の => のいずれかの側)とある。

やっぱりナンノコッチャ?と思った人がいるらしく、StackOverflow に What does the tt metavariable type mean in Rust macros? という質問が上がってた。

その回答の例によると、

fn main() {
    println!("Hello world!");
}

は以下のトークン木として認識される。

  • fn
  • main
  • ()
  • { println!("Hello world!"); }
    • println
    • !
    • ("Hello world!")
    • "Hello world!"
    • ;

つまり (), [], {} で囲まれたものはサブツリーになり、それ以外のものは葉になる。こういう木構造として認識できるものにマッチするらしい。ただし上の例は4つの木でできている。tt にマッチするのは a single token tree なので、この例はマッチしない。

macro_rules! match_tt {
    ( $t:tt ) => (println!("tt: {}", stringify!($t)))
};

// これはエラーになる!
// error: no rules expected the token `main`
match_tt!(
    fn main() {
        println!("Hello world!");
    }
);

// これはOK
match_tt!({ println!("Hello world!"); } );

上記は fn の次の main の時点でエラーになる。これは木が1つじゃないから、2つ目が見つかった時点でエラーになっている。うまくいく例を以下に挙げる。

match_tt!(i32);
match_tt!(_);
match_tt!(10);
match_tt!({ 10 });
match_tt!({
        let x = 10;
        println!({}, x);
    });
match_tt!({ name:[ tag1(), tag2() ] });
match_tt!({
    values: [
        1, 2, 3, 4
    ],
    kv: {
        "prefecture" => "Aichi",
        "city" => "Nagoya",
    }
});

例えば { name:[ tag1(), tag2() ] } は以下のような木構造になる。これは1つの木である。

  • { name:[ tag1(), tag2() ] }
    • name
    • :
    • [ tag1(), tag2() ]
      • tag1
      • ()
        • φ
      • ,
      • tag2
      • ()
        • φ

meta

アトリビュートの中身にマッチする。

macro_rules! match_meta {
    ( $m:meta ) => (println!("meta: {}", stringify!($m)))
};

// #[test]
match_meta!(test);
// #[inline(always)]
match_meta!(inline(always));
// #[cfg(target_os = "macos")]
match_meta!(cfg(target_os = "macos"));

指定子以外はそのまま文字列にマッチ

下記の "value => " はそのまま文字列としてマッチする。ただし空白は無視される。

macro_rules! foo {
    (value => $e:expr) => (println!("value = {}", $e));
}

fn main() {
    foo!(value => 1);  // println!("value = {}", 1);
    
    // 空白は無視されるっぽい
    foo!(value=>1);
}

複数条件

もちろんマッチ条件は複数指定できる。

macro_rules! foo {
    (x => $e:expr) => (println!("mode X: {}", $e));
    (y => $e:expr) => (println!("mode Y: {}", $e));
}

fn main() {
    foo!(x => 1);  // println!("mode X: {}", 1);
    foo!(y => 3);  // println!("mode Y: {}", 3);
}

繰り返し

以下のマッチャー部分で $( ),* と書かれた部分は、コンマで区切られた繰り返しを指定している。右側でも同様に $(),* でコンマで区切られた繰り返しとして展開している。

macro_rules! array{
    ( $( $x:expr ),* ) => ( [ $( $x ),* ] )
}

fn main() {
    let ar = array!(1, 2, 3, 4);  // let ar = [1, 2, 3, 4];
    for v in ar.iter() {
        println!("{}", v);
    }
}

* の前に書く文字が区切り文字。コンマじゃなくてもいいし、指定しなくてもいい。

// 区切り文字が ; なだけで vec! と同じ動作のマクロ。展開時は区切り文字なし。
macro_rules! myvec{
    ( $( $x:expr );* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

fn main() {
    let v = myvec![1; 2; 3; 4];  // let v = vec![1, 2, 3, 4]; と同じ
    println!("{:?}", v);  // [1, 2, 3, 4]
}

myvec! マクロを展開すると以下のようになっている。

// マクロを展開したもの
let v = {
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec.push(4);
    temp_vec
};

繰り返しを入れ子にすることもできる。また0回以上でなく1回以上の繰り返しなら * でなく + が利用できる。

macro_rules! o_O {
    ( $( $x:expr; [ $( $y:expr ),* ]);+) => {
        &[ $($( $x + $y ),*),+ ]
    }
}

fn main() {
    let a: &[i32] = o_O!(10; [1, 2, 3];
                         20; [4, 5, 6]);
    assert_eq!(a, [11, 12, 13, 24, 25, 26]);
}

再帰呼び出し

マクロは他のマクロはもちろん、自身を再帰的に呼び出すこともできる。

macro_rules! write_html {
    ($w:expr, ) => (());

    ($w:expr, $e:tt) => (write!($w, "{}", $e));

    ($w:expr, $tag:ident [ $($inner:tt)* ] $($rest:tt)*) => {{
        write!($w, "<{}>", stringify!($tag));
        write_html!($w, $($inner)*);
        write!($w, "</{}>", stringify!($tag));
        write_html!($w, $($rest)*);
    }};
}

fn main() {
    use std::fmt::Write;
    let mut out = String::new();

    write_html!(&mut out,
        html[
            head[title["Macros guide"]]
            body[h1["Macros are the best!"]]
        ]);

    assert_eq!(out,
        "<html><head><title>Macros guide</title></head>\
         <body><h1>Macros are the best!</h1></body></html>");
}

動作を理解するために簡単なものから考えてみよう。

write_html!(&mut out, html[]);

この場合、まず

    ($w:expr, $tag:ident [ $($inner:tt)* ] $($rest:tt)*) => {{

にマッチする。"html" は ident で識別子として認識される。その後は $inner, $rest ともに繰り返し0回と認識され、マッチが完了する。展開すると

write!($w, "<{}>", stringify!(html));
write_html!($w, );
write!($w, "</{}>", stringify!(html));
write_html!($w, );

となるが、中で write_html! 自身を再帰的に呼び出しているため、ここも展開される。この場合はどちらも

    ($w:expr, ) => (());

にマッチして、最終的に以下に展開される。

write!($w, "<{}>", stringify!(html));
();
write!($w, "</{}>", stringify!(html));
();

結果として第1引数に渡した out に

<html></html>

が出力される。

トークン木をパースしてみる

ちょっと自分で何か作ってみようかと。tt がトークン木をどのように認識するのかを確認するために、トークン木をパースして表示してみる。

まずは木を構成する要素を enum で定義する。サブツリーの中身が空の場合を表すものとして Empty を定義してもいいんだけど、中身が空の {}, (), [] は葉として扱うことにする。

enum Tree<'a> {
    Node(Vec<Tree<'a>>, &'a str),
    Leaf(&'a str),
}

Node も Leaf も表示文字列を保持している。文字列は String としてヒープに確保せず、折角なので Rust の特性を活かして、速度とメモリ使用量の効率を重視して文字列のスライスとして扱う。そのためライフタイムを指定する必要がある(enum が存在してる間は参照先が解放されたら困るので)。

以下は Tree の内容を画面に表示する関数。サブツリーはインデントを入れて階層が分かるように表示する。

fn print_tree(tree: &Tree) {
    fn print_with_indent(level: u32, text: &str) {
        for _ in 0..level { print!("  "); }
        println!("{}", text);
    }

    fn print_tree_with_level(level: u32, tree: &Tree) {
        match tree {
            &Tree::Node(ref nodes, desc) => {
                print_with_indent(level, desc);
                for node in nodes {
                    print_tree_with_level(level + 1, &node)
                }
            },
            &Tree::Leaf(leaf) => print_with_indent(level, leaf)
        }
    }

    print_tree_with_level(0, tree);
}

以下がパーサ。

macro_rules! parse_subtree {
    // v: 格納先, desc: このサブツリーを表す文字列, tree(s): サブツリーの中身
    ($v:expr, $desc:expr, $($tree:tt)+) => {
        let mut subtree = Vec::new();
        $(
            parse_token_trees!(subtree, $tree);
        )*
        $v.push(Tree::Node(subtree, $desc));
    }
}

macro_rules! parse_token_trees {
    // { } で囲まれたサブツリーにマッチ
    ($v:expr, {$($tree:tt)+}) => {
        parse_subtree!($v, stringify!( {$($tree)*} ), $($tree)*);
    };

    // ( ) で囲まれたサブツリーにマッチ
    ($v:expr, ($($tree:tt)+)) => {
        parse_subtree!($v, stringify!( ($($tree)*) ), $($tree)*);
    };

    // [ ] で囲まれたサブツリーにマッチ
    ($v:expr, [$($tree:tt)+]) => {
        parse_subtree!($v, stringify!( [$($tree)*] ), $($tree)*);
    };

    // 葉にマッチ
    ($v:expr, $leaf:tt) => {
        $v.push(Tree::Leaf(stringify!($leaf)));
    };

    // 複数のトークン木にマッチ
    ($v:expr, $($tree:tt)*) => {
        $(
            parse_token_trees!($v, $tree);
        )*
    };
}

パースして表示すると、

let mut trees = Vec::new();
parse_token_trees!(&mut trees,
    fn main() {
        println!("Hello world!");
    }
    );
for tree in trees { print_tree(&tree); }

以下のように表示される。4つのトークン木として認識されていることが分かる。

fn
main
(  )
{ println ! ( "Hello world!" ) ; }
  println
  !
  ( "Hello world!" )
    "Hello world!"
  ;
  1. コンパクトにまとめたかったのに長くなりすぎたので公開はしないかも。ドキュメントもその日本語訳も充実してますし。

  2. マクロの引数は括弧で囲めとか、マクロ内で宣言する変数は絶対に利用されないような名前にしろとか。

239
172
1

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
239
172

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?