セバスチャンマクロを作って学ぶRustの手続きマクロ
皆様、ごきげんよう。
わたくし、Rustaceanお嬢様の白山風露と申しますわ[1]。
本日はセバスチャン
[2]マクロの作成を通して、皆様と一緒にRustの手続きマクロの作り方を学んでいこうと思いますの。
手続きマクロとは何ですの?
皆様の中にはそんな疑問を抱いている方もいらっしゃるのではないかしら?
手続きマクロは、英語の"Procedural macro"の訳語ですわ。
こちらはRustのバージョン1.15.0で安定化された、以前は"Macros 1.1"なんて呼ばれていた機能ですの。
手続きマクロが1.1なのですから、以前にもRustにはマクロが存在しておりまして、macro_rules!
マクロ[3]を使って定義するのがそれになりますわ。
macro_rules!
を使ったマクロ定義はトークン列をパターンマッチして置き換えルールを記述するものですが、手続きマクロはトークン列を受け取ってトークン列を返す関数を定義することで実装することができますわ。つまり、コンパイル中に自作の関数でソースコードの一部をある程度自由に置き換えることができるのですわ。
手続きマクロ用クレートの作り方を説明いたしますわ。
手続きマクロを作るには、手続きマクロ用クレートを作る必要がありますわ。
手続きマクロ用クレートはライブラリクレートの一種ですが、手続きマクロ以外の定義を公開することができませんの。ですから、既存のライブラリに手続きマクロを追加したくなった場合などは、ワークスペース機能を使って手続きマクロ用クレートを追加する必要がありますわ。
手続きマクロ用クレートは、Cargo.toml
の[lib]
にproc-macro = true
と書くことで作成できますわ。
手続きマクロ用クレートの中ではproc_macro
クレート様と、
#[proc_macro_derive]
#[proc_macro_attribute]
#[proc_macro]
の3つの属性が使えるようになりますの。これらは手続きマクロ用クレート以外からは呼び出すことができない特殊なクレートと属性になりますわ。
proc_macro
クレート様でできることと、それぞれの属性は以下で説明いたしますわ。
手続きマクロにも種類がございますのよ。
手続きマクロには
- カスタム
#[derive]
マクロ - 属性風マクロ
- 関数風マクロ
の3種類がございますわ。
これらは定義方法は大体同じなのですが、使う場所が異なりますわ。
#[derive]
マクロの説明ですわ。
カスタムカスタム#[derive]
マクロは#[derive(Debug)]
などと同じように、自作のトレイトに対して自動でトレイト境界を定義するために使われますわ。
例えばserde
クレート様にはSerialize
とDeserialize
のカスタム#[derive]
マクロが定義されていますの[4]。JSONなどの形式で入出力を行うためにお世話になった方も多いのではございませんかしら?
カスタム#[derive]
マクロは#[proc_macro_derive]
属性を付けて、proc_macro::TokenStream
を受け取り、proc_macro::TokenStream
を返す関数として定義いたしますわ。
// crate my_macro
use proc_macro::TokenStream;
#[proc_macro_derive(MyTrait)]
pub fn my_trait_derive(tokens: TokenStream) -> TokenStream {
// ...
}
このように定義すると、外側からはuse my_macro::MyTrait;
と書いて#[derive(MyTrait)]
のように使うことができますわ。
なお、先ほど述べましたように、手続きマクロ用クレートからは手続きマクロ以外を公開することができませんので、trait MyTrait
は別のライブラリクレート内で定義しておく必要がありますわ。
カスタム#[derive]
マクロは入力としてはstruct
かenum
の定義をトークン列として受け取るのですが、この後説明する他の手続きマクロと異なりまして、入力となるソースコード部分を置き換えるのではなく、出力部分がソースコードに追加される形になりますわ。ですので、カスタム#[derive]
マクロを使ってstruct
やenum
の定義そのものを書き換えることはできませんわ。
ヘルパー属性についてですわ。
#[proc_macro_derive]
属性は第一引数に公開される名前を指定するのですが、第二引数にattributes(my_trait)
などのようにヘルパー属性の名前を指定できますわ。
#[proc_macro_derive(MyTrait, attributes(my_derive_attr))]
pub fn my_trait_derive(tokens: TokenStream) -> TokenStream {
// ...
}
このようにいたしますと、以下のように書いたとき、
#[derive(MyTrait)]
struct MyStruct {
#[my_derive_attr]
i: i32,
}
#[my_derive_attr]
属性がマクロ呼び出し前の構文解析でエラーにならなくなりますの。もちろん構文解析でエラーにならないだけで、属性をどのように解釈するかは手続きマクロの実装次第ですので、手続きマクロがエラーを出せばエラーになることもありますわ。
例えばserde
クレート様で#[derive(Serialize)]
などを使うと#[serde]
属性が使えるようになるのはこの機能を使っておりますわ。
属性風マクロの説明ですわ。
属性風マクロはRust組み込みの属性(例えば#[cfg]
や#[test]
など)のように、任意のアイテム[5]に対して付けることができる属性を定義できる機能ですわ。
属性風マクロは入力として受け取ったトークン列を出力のトークン列で置き換えますわ。ですので、カスタム#[derive]
マクロよりも柔軟ですのよ。
属性風マクロは以下のように#[proc_macro_attribute]
属性を使用して定義できますわ。
#[proc_macro_attribute]
pub fn my_attr(attrs: TokenStream, item: TokenStream) -> TokenStream {
// ...
}
カスタム#[derive]
マクロと異なり、入力が2つになっていますわね。
これは属性が引数を受け取ることができるためでして、
#[my_attr(a, b, c)]
fn my_fn() {}
などと書きますと、a, b, c
の部分がattrs
引数に、fn my_fn() {}
の部分がitem
引数に渡されることになりますわ。
関数風マクロの説明ですわ。
関数風マクロは、println!
などのmacro_rules!
を使って定義されたマクロと同じように使えるマクロですわ。
関数風マクロを定義には#[proc_macro]
属性を使いますわ。属性が引数をとることもありませんし、入力も引数1つの一番シンプルな形ですのよ。
#[proc_macro]
pub fn my_macro(tokens: TokenStream) -> TokenStream {
// ...
}
カスタム#[derive]
マクロはstruct
やenum
に付ける形ですし、属性風マクロはアイテムに付ける形になりますので、必然的にマクロに渡されるトークン列はRustの文法的に正しくなければなりませんでしたわ。
一方で、関数風マクロは、括弧の対応や使える文字に制限はございますが、Rustとは文法的にかけ離れたトークン列でも入力に受け付けることができますわ[6]。
今回、セバスチャン
マクロはこの関数風マクロを使って作っていきますわ。
クレートの初期化をいたしますわ。
それでは皆様、まずはマクロ用クレートを作りましょう。そうですわね……、ここはお嬢様らしくクレート名はreijou
といたしましょうか。
クレートを作るには以下のコマンドを実行しますわ。
cargo new --lib reijou
これでとりあえずライブラリクレートが作られましたわ。一旦コミットを行いましょう。
この状態ではまだ普通のライブラリクレートですわ。そこで、Cargo.tomlを開いて、以下の内容を追加しますわよ。
[lib]
proc-macro = true
ここでcargo build
をいたしますと、以下のようなエラーが出てくるのではなくて?
error: `proc-macro` crate types currently cannot export any items other than functions tagged with `#[proc_macro]`, `#[proc_macro_derive]`, or `#[proc_macro_attribute]`
--> src\lib.rs:1:1
|
1 | pub fn add(left: usize, right: usize) -> usize {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: could not compile `reijou` due to previous error
先ほども申しました通り、手続きマクロ用クレートからはマクロ以外を公開することができませんので、cargo new
で生成されたテンプレートのままではエラーになってしまうのですわ。
ですので、add
関数は削除いたしまして、セバスチャン
マクロを追加いたしますわ。
use proc_macro::TokenStream;
#[proc_macro]
pub fn セバスチャン(tokens: TokenStream) -> TokenStream {
todo!()
}
add
関数を削除したことでテストもビルドに失敗するようになってしまいましたので、こちらも変更しますわ。
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_セバスチャン() {
todo!()
}
}
とりあえずこんな感じにいたしますわ。警告は出ますものの、ビルドには成功するようになりましたわね。中身がtodo!()
になっているので当然テストが通らないのですが、ここで一旦コミットいたしますわ。
テストができるようにいたしますわ。
さて、ここで残念なお話があるのですが、proc_macro::TokenStream
を利用したコードはテストができませんの。
先ほどコミットした状態からtodo!()
を消しまして、試しに入力をそのまま返すように変更してみますわ。
#[proc_macro]
pub fn セバスチャン(tokens: TokenStream) -> TokenStream {
tokens
}
TokenStream::new()
で空のTokenStream
を作れますから、
#[test]
fn test_セバスチャン() {
セバスチャン(TokenStream::new());
}
といたしますと、一旦テストが通りそうに思えます。ところが、これでcargo test
を実行いたしますと、
running 1 test
test tests::test_セバスチャン ... FAILED
failures:
---- tests::test_セバスチャン stdout ----
thread 'tests::test_セバスチャン' panicked at 'procedural macro API is used outside of a procedural macro', library\proc_macro\src\bridge\client.rs:350:17
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::test_セバスチャン
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass '--lib'
このようにテストに失敗するのですわ。
エラーメッセージに出ておりますように[7]、手続きマクロのAPIは手続きマクロの外では使えませんわ。この場合テストコードも手続きマクロの外ということになってしまいますの。
しかしこれではあまりに不便ですわ。エレガントなテストはRustaceanお嬢様の嗜みですのよ。
ですが心配はいりませんわ。手続きマクロの内部からしか使えないproc_macro
クレート様に代わって、proc_macro
クレート様とほぼ同じAPIが実装されたproc-macro2
クレート様というライブラリクレートが存在しますの。
proc_macro2::TokenStream
はproc_macro::TokenStream
と相互に変換が可能でして、普通のライブラリクレートなのでテストから呼び出してもpanicを起こしませんの。
ですので、#[proc_macro]
属性を付けた関数はエントリーポイントとしてproc_macro::TokenStream
とproc_macro2::TokenStream
の変換のみを行い、それ以外の処理はproc_macro2::TokenStream
を受け取る実装関数に転送してしまえば、そちらをテストすることができるようになりますわ。
proc-macro2
クレート様に加えてsyn
クレート様とquote
クレート様は手続きマクロの作成にとても便利なクレートですので、合わせてこの3つをCargo.toml
のdependencies
に書き加えますわ。
[dependencies]
proc-macro2 = "1.0.40"
syn = "1.0.98"
quote = "1.0.20"
バージョンはこの記事の執筆時点の最新版ですわ。皆様は適宜バージョンを読み替えてくださいな。
新しくセバスチャン_impl
関数を用意しまして、セバスチャン
関数から呼び出すようにいたしましょう。
use proc_macro2::TokenStream;
fn セバスチャン_impl(tokens: TokenStream) -> TokenStream {
tokens
}
#[proc_macro]
pub fn セバスチャン(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
セバスチャン_impl(tokens.into()).into()
}
テストコードもセバスチャン_impl
を呼び出すようにいたしますわ。
#[test]
fn test_セバスチャン() {
セバスチャン_impl(TokenStream::new());
}
これでcargo test
が通ることを確認してくださいまし。
確認いたしましたら、ここでコミットをいたしますわ。
テストを書きますわ。
ここまで来ましたら、ようやくマクロの中身の実装に入れますわ。ですがここはやはり、テストファーストで行きたいところですわね。TDDは乙女の嗜みですわ。
テストを書くにあたって、pretty_assertions
クレート様を導入させていただきますわ。pretty_assertions::assert_eq!
を使用いたしますと、テストに失敗した時に、差分をdiff形式で表示してくださいますわ。Cargo.toml
に以下を追加しますわ。
[dev-dependencies]
pretty_assertions = "1.2.1"
例によってバージョンは適宜読み替えてくださいませ。
そういえば、皆様にはセバスチャン
マクロがどのような機能を目指しているか説明していませんでしたわ。セバスチャン
マクロとは、セバスチャンに説明することでRustのコードが書けるマクロですわ。具体的に言うと、
セバスチャン! {
わたくし std::env::args 様を使わせていただきますわ.
}
と書きますと、
use std::env::args;
に置き換えられる、といった機能を目指しておりますの。
今申しましたことをそのままテストにいたしますわ。
つまり、わたくし std::env::args 様を使わせていただきますわ.
をTokenStream
にしたものをセバスチャン_impl
に渡しまして、結果がuse std::env::args;
をTokenStream
にしたものと一致すれば良いので、そのようなテストを書くのですわ。
先ほどproc-macro2
クレート様と一緒に追加したquote
クレート様にはquote!
マクロがございまして、これを利用いたしますと
quote!{
わたくし std::env::args 様を使わせていただきますわ.
}
のように書くことでTokenStream
を生成できるのですわ。
TokenStream
はPartialEq
を実装しておりませんので、そのままだと比較できませんわ。そこで、to_string
を呼んでからassert_eq!
に渡すことになりますわ。
つまり、以下のようになりますわね。
use quote::quote;
#[test]
fn test_セバスチャン() {
assert_eq!{
セバスチャン_impl(quote!{
わたくし std::env::args 様を使わせていただきますわ.
}).to_string(),
quote!{
use std::env::args;
}.to_string()
};
}
さて、セバスチャン_impl
は今のところただ入力をそのまま返すだけの関数になっていますわ。ですからこのテストは当然落ちるはずですわ。試しにcargo test
を実行してみますと、
このようにカラフルなdiffが表示されるはずですわ。これでテストに失敗した時にどこが原因か一目瞭然になるのですわ。
ここらへんで一旦でコミットを作りますわ。
マクロの中身の実装ですわ。
それでは、ようやくマクロの中身を実装して、このテストが通るようにしていきますわよ。
TokenStream
を解析するにはsyn
クレート様を使いますわ。
そうそう、今回使わせていただきますsyn
クレート様の一部の機能はfeatures
で指定しておかないと使えませんので、Cargo.toml
をこんな風に書き換えますわ。
-syn = "1.0.98"
+syn = { version = "1.0.98", features = ["full"] }
syn
クレート様にはparse::Parser
というトレイトがありまして、Fn(syn::parse::ParseStream) -> syn::Result<TokenStream>
型に対して実装がありますわ。つまり、ParseStream
を受け取ってResult<TokenStream>
を返す関数を実装いたしますと、その関数に対してParser
トレイトのメソッドを呼び出すことができるのですわ。
ですので、まずこのようにセバスチャン_parse
関数を定義いたしまして、
use syn::{
parse::{Parser, ParseStream},
Error, Result,
};
fn セバスチャン_parse(input: ParseStream) -> Result<TokenStream> {
todo!()
}
セバスチャン_impl
関数からParser::parse2
メソッドを呼び出しますわ。
fn セバスチャン_impl(tokens: TokenStream) -> TokenStream {
セバスチャン_parse.parse2(tokens)
.unwrap_or_else(Error::into_compile_error)
}
セバスチャン_parse
関数がinput: ParseStream
を受け取っているのが重要でして、こちらを利用いたしますとトークンの先読みや部分的なパースなどができるようになるのですわ。syn::parse::ParseStream
は定義を見ていただければ、
pub type ParseStream<'a> = &'a ParseBuffer<'a>;
となっておりますことからsyn::parse::ParseBuffer
型の参照のエイリアスでして、ParseBuffer
のメソッドを利用することになりますわ。
また、戻り値がResult
になりますので、エラーが発生した時は?
演算子で抜けることができるのも利点ですわね。戻り値がErr
だった時は、セバスチャン_impl
側でsyn::Error::into_compile_error
によってコンパイルエラーに変換いたしますわ。
さて、今の目標はわたくし std::env::args 様を使わせていただきますわ.
という入力をuse std::env::args;
に変換することですわ。ですので、入力の要素がRustのトークンとしては何になるかを考える必要がありますのよ。
まず、わたくし
はRustのトークンとしては識別子になりますわ。識別子はproc_macro2::Ident
型で表現されますわ。
ParseBuffer::parse
を利用すると、syn::parse::Parse
トレイト境界が実装されている型をパースすることができるようになりますわ。proc_macro2
で用意されている構文要素にはそれぞれParse
トレイトが実装されていますので、
use proc_macro2::Ident;
let ident: Ident = input.parse()?;
といたしますとIdent
が一つパースされ、input
の内部状態がパースが終わった位置に進みますわ[8]。
Ident
をパースして、実際は識別子以外のトークンがあった場合はエラーになりますわ。ですが、Ident
は識別子全般をパースいたしますので、わたくし
以外の識別子でもパースできてしまいますわ。
そこで、Ident
がわたくし
でなかった場合はコンパイルエラーにしてしまおうと思いますわ。Ident
にはToString
トレイト境界が実装されておりますので、ident.to_string() == "わたくし"
で判定できますわね。
if ident.to_string() == "わたくし" {
todo!()
} else {
return Err(Error::new(
ident.span(),
format!("予期せぬ識別子ですわ~!: {}", ident),
));
}
エラー処理の部分は今後も同じようなことを何度も書くことになりそうですので、関数に切り出しておくと良さそうですわ。
fn unexpected_ident<T>(ident: &Ident) -> Result<T> {
Err(Error::new(
ident.span(),
format!("予期せぬ識別子ですわ~!: {}", ident),
))
}
わたくし
以外の識別子がコンパイルエラーを引き起こすことを確認するためにテストを追加してみますわ。
#[test]
fn test_unexpected_ident() {
assert_eq! {
セバスチャン_impl(quote!{
わたし
}).to_string(),
quote!{
compile_error!{ "予期せぬ識別子ですわ~!: わたし" }
}.to_string()
};
}
cargo test test_unexpected_ident
を実行して通ることを確認いたしましょう。
わたくし
がパースできましたら、続いてはstd::env::args
に該当する部分をパースしますわ。こちらはRustの構文要素としてはUseTreeと定義されております部分で、syn::UseTree
型で表現されますわ。
let tree: UseTree = input.parse()?;
UseTree
の後ろには様を使わせていただきますわ
という識別子と.
トークンが続く必要がありますわ。識別子はわたくし
の時と同じようにチェックいたしますが、少し汎用的にするために以下のような関数を作りますわ。
fn expect_ident(input: ParseStream, name: &str) -> Result<Ident> {
let ident: Ident = input.parse()?;
if ident.to_string() == name {
Ok(ident)
} else {
unexpected_ident(&ident)
}
}
このようにしておきますと、
expect_ident(input, "様を使わせていただきますわ")?;
といたしますことで特定の識別子が来ることを確認できますわ。
続いて、.
トークンの確認ですわ。使える文字種の関係でこの位置に。
を登場させることができないのでその代わりですわね。、
の場合も代わりに,
を使いますわ。わたくしは句読点は「。、」派ではございますけど、論文など「.,」を使って書かれることもありますから問題ありませんわね。
.
トークンはsyn::token::Dot
型で表されるのですが、syn
クレート様はsyn::Token!
マクロを使ってToken![.]
と表現することをご推奨なさっておいでですので、その通りにいたしますわ。
input.parse::<Token![.]>()?;
.
トークンがこの位置に存在することが確かめられればよいので、戻り値は使いませんわ。
さて、後はtree
を使ってTokenStream
を作りましょう。TokenStream
の作成には、先ほどテストを書く時にも使ったquote!
マクロを使用いたしますわ。
quote!
マクロにはmacro_rules!
と似たような書き方で、変数に束縛されたToTokens
トレイト境界が実装された型を埋め込むことができる機能がございますの。macro_rules!
が$
を使うのに対して、quote!
マクロでは#
を使うのが大きな違いですわね。
ここでは、
quote!{
use #tree;
}
と書くことでtree
に束縛されたUseTree
の値をTokenStreamの中に展開できますわ。
ここまでをまとめると、以下のようなコードになりますわ。
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use syn::{
parse::{ParseStream, Parser},
Error, Result, Token, UseTree,
};
fn unexpected_ident<T>(ident: &Ident) -> Result<T> {
Err(Error::new(
ident.span(),
format!("予期せぬ識別子ですわ~!: {}", ident),
))
}
fn expect_ident(input: ParseStream, name: &str) -> Result<Ident> {
let ident: Ident = input.parse()?;
if ident.to_string() == name {
Ok(ident)
} else {
unexpected_ident(&ident)
}
}
fn セバスチャン_parse(input: ParseStream) -> Result<TokenStream> {
let ident: Ident = input.parse()?;
if ident.to_string() == "わたくし" {
let tree: UseTree = input.parse()?;
expect_ident(input, "様を使わせていただきますわ")?;
input.parse::<Token![.]>()?;
Ok(quote! {
use #tree;
})
} else {
return unexpected_ident(&ident);
}
}
fn セバスチャン_impl(tokens: TokenStream) -> TokenStream {
セバスチャン_parse
.parse2(tokens)
.unwrap_or_else(Error::into_compile_error)
}
そしてこの時点でテストが通るようになっているはずですわ。cargo test
を実行してテストが通ることを確認いたしましょう。
テストが通りましたら、コミットをいたしますわ。
試しに使用してみますわ。
ここまでくればセバスチャン
マクロを使ってみることができるようになりましたわ。
exemples/example1.rs
というファイルを作成しまして、Cargo.toml
に以下を追加いたしますわ。
[[example]]
name = "example1"
path = "examples/example1.rs"
examples/example1.rs
の中身はこんな感じにいたしますわ。
use reijou::セバスチャン;
セバスチャン! {
わたくし std::env::args 様を使わせていただきますわ.
}
fn main() {
for arg in args() {
println!("{}", arg)
}
}
use reijou::セバスチャン;
の部分を
セバスチャン! {
わたくし reijou::セバスチャン 様を使わせていただきますわ.
}
にできないのは少し残念ですわね。でもわたくしがセバスチャンのことを様付けで呼ぶのも少し妙な気がいたしますから、これで良いのかもしれませんわ。
コードの内容的には、単純に受け取った引数を一行ずつ出力するだけですわ。cargo run --example example1
で実行できますわ。
また一旦コミットいたしましょう。
機能を増やしますわ。
皆様もここまでで基本的な手続きマクロの作り方を理解できたと思いますが、これだけだと少し機能的に寂しいですわ。
そこで、簡単な関数定義をできるよう機能を拡張しようと思いますわ。
目標は、
セバスチャン! {
こちらの f 様は,
a: i32 と b: &str をお受け取りになって,
std::io::Result<()> をお返しになり,
以下のことをなさいますのよ. {
writeln!(std::io::stdout(), "a: {}", a)?;
writeln!(std::io::stdout(), "b: {}", b)?;
Ok(())
}
}
を、
fn f(a: i32, b: &str) -> std::io::Result<()> {
writeln!(std::io::stdout(), "a: {}", a)?;
writeln!(std::io::stdout(), "b: {}", b)?;
Ok(())
}
に置き換えできるようにすることですわ。
テストを追加しますわ。
実装をはじめる前に、テストを追加いたしましょう。テストファーストはRustaceanお嬢様の嗜みですわ。
先ほどと同じように、上で申しましたことをそのままテストの内容にいたしましょう。
#[test]
fn test_fn() {
assert_eq! {
セバスチャン_impl(quote!{
こちらの f 様は,
a: i32 と b: &str をお受け取りになって,
std::io::Result<()> をお返しになり,
以下のことをなさいますのよ. {
writeln!(std::io::stdout(), "a: {}", a)?;
writeln!(std::io::stdout(), "b: {}", b)?;
Ok(())
}
}).to_string(),
quote!{
fn f(a: i32, b: &str) -> std::io::Result<()> {
writeln!(std::io::stdout(), "a: {}", a)?;
writeln!(std::io::stdout(), "b: {}", b)?;
Ok(())
}
}.to_string()
};
}
そういえば、 先ほどのtest_セバスチャン
というテスト名はテストが増えてくるとあまり適切ではなく思えますわね。test_use
とでも変更しましょうか。
またテストが失敗するようになりましたが、ここで一旦コミットいたしますわ。
追加機能の実装ですわ。
テストを追加いたしましたので、追加機能を実装していきたいところですが、そうですわね……、ここは皆様も実装してみませんこと?
皆様はhttps://github.com/kazatsuyu/reijouリポジトリのbefore-fn
タグから実装をはじめてみてくださいませ。
わたくしの実装結果はこちらのコミットに置いておきますわ。
更なる機能追加もできれば良いですわね。
これで簡単な関数は作れるようになりましたが、まだ機能を追加しようと思えばできますわ。
たとえば、関数本体のブロックは何も手を加えていないので、こちらももっとお嬢様らしくエレガントに記述できるようにする、ですとか、struct
やenum
なども定義できるようにする、などですわ。
(とはいえ、まあ正直に言ってしまえばこれはジョークライブラリですので、あまりこだわっても不毛だと思いますわ)
おわりにですわ。
いかがだったかしら?
皆様がこれで手続きマクロを作れるようになりますと、わたくしも嬉しく思いますわ。
最後になりましたが、今回の記事の発想の元になりましたのは、@yaito3014様の以下のツイートのツリーですわ。一部の文言など勝手ながら参考にさせていただきましたの。感謝いたしますわ。
ここまでお読みになって下さりありがとうございますわ。
それでは皆様、ごきげんよう。
-
わたくしはお嬢様ですわ。どなたが何をおっしゃろうともお嬢様なのですわ。 ↩︎
-
もちろん当家の執事の名前ですわ。 ↩︎
-
余談ですが、
macro_rules!
をマクロと言ってしまっていいのか少し疑問がありますわ。通常のマクロ呼び出しとも文法的に異なりますし。とは言え公式様のRust By Exampleでも"macro_rules!
macro"と呼んでいらっしゃるのでそれに倣うことにいたしますわ。 ↩︎ -
使えるようにするには
Cargo.toml
の[dependencies.serde]
にfeatures = ["derive"]
と書く必要がありますわ。 ↩︎ -
アイテムはRustの構文要素のカテゴリの一つですわ。クレートの外に公開できる
struct
やenum
、fn
などが該当しますの。式に付けることはできませんので、ブロックなどに付けることができる#[cfg]
よりは制限がありますわね。 ↩︎ -
強力な表現力を得られます一方、知らないマクロを見た時に「これはなんですの~!!!」となってしまいますわね。 ↩︎
-
'procedural macro API is used outside of a procedural macro'の部分ですわ。 ↩︎
-
input
にはmut
が付いていないのに内部状態が変わるのは少し不思議ですわね。わたくし、そのうち内部構造をきちんと調べてみたいですわ。 ↩︎
Discussion