proc_macro_workshopでRustの手続き的マクロに入門する 前編

はじめに

この記事では proc_macro_workshop というリポジトリを使って Rust の手続き的マクロの作り方を学んでいきます。想定している読者は以下のような方です。

  • Rust の基本的な文法や概念(トレイトや所有権、ライフタイムなど)を知っている
  • 手続き的マクロの作り方について知りたい

この記事では以下のことを説明します。

  • Rust のマクロの概要
  • 手続き的マクロ(deriveマクロ)の作り方
  • proc_macro_workshop の進め方

また、この記事では以下のことは説明しません

  • Rust の基本的な文法や概念
  • 宣言的マクロの作り方

Rust のマクロ

Rust のマクロには宣言的マクロと手続き的マクロの 2 種類があります。

宣言的マクロ

宣言的マクロは macro_rules! 構文を使用して定義されるマクロで、match 式に似た形で処理内容を定義します。 vec!println! など普段良く使うマクロも宣言的マクロです。

宣言的マクロについてはThe Rust Programming Language 日本語版vec!マクロを例にした解説があるので、そちらをご覧ください。

手続き的マクロ

手続き的マクロはトレイトのデフォルト実装の導出に用いる derive マクロなどに代表されるマクロです。宣言的マクロよりも複雑な処理を記述できることが特徴で、以下の 3 種類があります。

derive マクロ
構造体やenumに付与することでその構造体やenumに追加の処理を実装できます
関数風マクロ
関数風マクロは宣言的マクロのように関数呼び出しと似た形で使用できるマクロです。宣言的マクロと比較するとより複雑な処理を記述できます
属性風マクロ
deriveマクロと同様に付与した対象に対して追加の処理を実装できますが、こちらは構造体やenumだけでなく関数などにも適用できます。テストを書くときに用いる#[test]アトリビュートなどがこれにあたります

手続き的マクロについてはThe Rust Programming Language 日本語版により詳しい説明があるのですが、これだけで実際に使えるような手続き的マクロを作り始めるのは難しいかと思います。この記事ではproc_macro_workshop というリポジトリを使って実際に手を動かしながら手続的マクロの作り方を学んでいきます。

proc_macro_workshop

proc_macro_workshop について

proc_macro_workshop には作成するマクロの種類によって 5 つのプロジェクトがあります。

  • derive マクロ derive(Builder)
  • derive マクロ derive(CustomDebug)
  • 関数風マクロ seq!
  • 属性風マクロ #[sorted]
  • 属性風マクロ #[bitfield]

これらのマクロの内容についてはproc_macro_workshop リポジトリをご覧ください。

この記事では、Builder パターンに必要な実装を導出する derive(Builder) プロジェクトを進めつつ手続き的マクロの作成方法を学んでいきます。

derive(Builder) プロジェクトを進めると最終的には以下のような使い方のできるマクロが完成します。

use derive_builder::Builder;

#[derive(Builder)]
pub struct Command {
    executable: String,
    #[builder(each = "arg")]
    args: Vec<String>,
    #[builder(each = "env")]
    env: Vec<String>,
    current_dir: Option<String>,
}

fn main() {
    let command = Command::builder()
        .executable("cargo".to_owned())
        .arg("build".to_owned())
        .arg("--release".to_owned())
        .build()
        .unwrap();
}

proc_macro_workshop の進め方

proc_macro_workshop の各プロジェクトは以下の流れで進めていきます。

  1. テストケースを追加する
  2. 追加したテストケースをパスするようなマクロを実装する
    • src/lib.rsに実装します
  3. テストをパスしたら 1.に戻る

proc_macro_workshop の各プロジェクトにはそれぞれのマクロがパスすべきテストが記載されており、これらのテストに通過するようなマクロを実装することが目標となります。derive(Builder) プロジェクトの場合はbuilder/testsディレクトリにテストケースが記載されたファイルが格納されています。各ステップでテストケースを 1 つずつ増やしていき、これに対応した機能を実装してきます。 プロジェクトの進捗はtests/progress.rsを用いて管理します。tests/progress.rsは以下のようになっており、各行がそれぞれのテストケースに対応しています。各ステップでテストケースのコメントアウトを解除し、それぞれのテストを通過するように実装を進めていきます。

#[test]
fn tests() {
    let t = trybuild::TestCases::new();
    t.pass("tests/01-parse.rs");
    //t.pass("tests/02-create-builder.rs");
    //t.pass("tests/03-call-setters.rs");
    //t.pass("tests/04-call-build.rs");
    //t.pass("tests/05-method-chaining.rs");
    //t.pass("tests/06-optional-field.rs");
    //t.pass("tests/07-repeated-field.rs");
    //t.compile_fail("tests/08-unrecognized-attribute.rs");
    //t.pass("tests/09-redefined-prelude-types.rs");
}

各テストケースにおける目標はテストケースが記載されているファイルのコメントに書いてあります。実際に作業する際はこれらのコメントを参考にしつつ実装していくことになります。

テストは builder ディレクトリでcargo testコマンドを実行することで実行できます。

derive(Builder) マクロを作る

derive(Builder)プロジェクトでは大まかに以下のような流れでマクロを作成していきます。

  1. マクロのひな型を用意する
    • 01-parse で作業します
  2. builder パターンを実現するために必要な機能(setter メソッドなど)を実装する
    • 02-create-builder ~ 05-method-chaining で作業します
  3. エラーハンドリングなどを実装する
    • 06-optional-field ~ 09-redefined-prelude-types で作業します

それでは実際にマクロを書いていきましょう。以降の各節の名前はテストファイルの名称と対応しています。以降の各節では以下の流れで進めていきます。

  1. ステップの目標を確認する
    • 実際には具体的なテストケースがありますが、すべて記載すると煩雑なので各ステップのゴールのみ記載します
  2. 実装方針の説明
  3. 実装
    • 各ステップの最後にテストケースをパスする実装例を提示します

マクロの処理の流れ

Rust の手続き型マクロの処理は基本的に以下のようになっています。本記事ではこの内容を具体的に実装していきます。

  1. トークン列を入力として受け取る
  2. 受け取ったトークン列を構文木に変換する
  3. 変換した構文木をもとに処理を行い所望の構文木を得る
  4. 得られた構文木トークン列に変換して返す

トークン列と構文木の相互変換にはsynクレートquoteクレートがよく使われます。この記事でもこれらのクレートを用いてマクロを実装します。

synクレートはトークン列から構文木への変換に使用します。また、synクレートには構文木に関する構造体が定義されており、パースして得られた構文木はこれらの構造体のインスタンスとして保持されます。quoteクレートは構文木からトークン列への変換に使用します。

マクロを開発する上での Tips

dbg!マクロの使用

マクロのデバッグをする際などには構文木の中身が実際にどうなっているかを知りたくなることがあるかもしれません。synクレートのextra-traitsフィーチャーを有効にするとこれらの構造体にDebugトレイトが実装されてdbg!マクロを使ったデバッグができます。開発する上で便利なので開発中は有効にしておくと良いでしょう。

syn = { version = "1.0.86", features = ["extra-traits"] }

cargo-expand

cargo expand を使うとマクロによって生成されたコードを出力できます。これもデバッグに便利なので詰まった時は使うと良いでしょう。proc-macro-workshop の GitHub リポジトリcargo-expand の GitHub リポジトリに使い方の説明があるので、使う場合はこちらを参照してください。

01-parse

目標

  • 空の derive マクロを作る

実装方針

テストが実行されるのはtests/01-parse.rsmain関数ですが、このステップでは何も記載がありません。ですので、特に何もしなくてもテストをパスする…かというとそうではなく、#[derive(Builder)]が使われた際に呼ばれる derive マクロが存在している必要があります。

なぜかというと、テストコード中で以下のように構造体に対して derive マクロが呼ばれているためです。

#[derive(Builder)]
pub struct Command {
    executable: String,
    args: Vec<String>,
    env: Vec<String>,
    current_dir: String,
}

マクロの基本的な処理内容についてはすでに説明しましたが、より具体的には以下のような流れになっています。

  1. トークン列proc_macro::TokenStream (以降では基本的にTokenStreamと記載します)を引数として受け取る
  2. syn::parse_macro_input!マクロでパースして構文木にする
  3. パースした構文木を元に所望の構文木を生成する
  4. 生成した構文木TokenStream に変換して返す

このステップではマクロが存在していればよいので、1.と 4.の処理を実装すればテストをパスします。

実装

それでは実際に実装をしていきます。

まずは derive マクロの定義から始めます。関数にproc_macro_deriveアトリビュートをつけることでその関数内に記載された処理が derive マクロの実装になります。 derive マクロのシグネチャ(TokenStream) -> TokenStreamになっており、実態は Rust のトークンをTokenStreamという構造体で受け取り、内部で処理した後にTokenStreamという構造体として返す関数です。

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {}

では関数の中身を実装してきましょう。最低限テストが通るためにはこの関数からTokenStreamを返せば良いので、以下のようにTokenStreamを新しく作って返してやれば OK です。

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    proc_macro::TokenStream::new()
}

このままでも良いのですが、次のステップへの準備として入力をパースできるようにもしておきましょう。入力のパースにはsyn::parse_macro_input!マクロを使います。derive マクロの入力はsyn::DeriveInputで定義される構造をしているのでDeriveInputとしてパースします。入力をパースする処理を追加すると最終的なマクロの実装は以下のようになります。

use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let _input = parse_macro_input!(input as DeriveInput);
    proc_macro::TokenStream::new()
}

02-create-builder

目標

  • CommandBuilder構造体のインスタンスを返すbuilder関数をCommand構造体に実装する

実装方針

最終的には以下のようなCommandBuilder構造体のインスタンスを返すbuilder関数をCommand構造体に実装するのが目的です。

pub struct CommandBuilder {
    executable: Option<String>,
    args: Option<Vec<String>>,
    env: Option<Vec<String>>,
    current_dir: Option<Vec<String>>,
 }

必要な作業は大きく分けて以下の 2 つです。

  • builder関数をCommand構造体に実装する
  • CommandBuilder構造体の定義を実装する
    • 汎用的に使えるようにするには Builder 構造体の構造体名やフィールドの名前、型は derive マクロが適用される構造体に応じたものにする必要がある

これを同時に全部進めるのはたいへんですので、以下の順に進めていきます。

  1. 空のbuilder関数をCommand構造体に実装する
  2. CommandBuilderを返すようにbuilder関数の実装を変更する
  3. 構造体名、フィールド名に応じた Builder 構造体を生成する

実装

空のbuilder関数をCommand構造体に実装する

前のステップで説明したように、手続き的マクロの処理の概要は以下の通りです。1.と 2.は前のステップで実装しました。このステップでは 3.と 4.の処理を実装していきます。 まずは空のbuilder関数を実装する処理を書いていきましょう。この段階では入力について気にする必要はありません。

  1. proc_macro::TokenStream (以降では基本的にTokenStreamと記載します)を引数として受け取る
  2. syn::parse_macro_input!マクロでパースして構文木にする
  3. パースした構文木を元に所望の構文木を生成する
  4. 生成した構文木TokenStream に変換して返す

トークン列を生成するためにはquoteクレートのquote!マクロを使います。以下のように Rust のコードと(ほぼ)同じ構文で記述でき、この内容がトークン列に変換されます。

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let _input = parse_macro_input!(input as DeriveInput);

    let expand = quote! {
        impl Command {
            pub fn builder() {}
        }
    };
    proc_macro::TokenStream::new()
}

このままだとproc_macro::TokenStream::new()によって生成された空のトークン列が戻り値として返るので、生成したトークン列を返すようにしましょう。quote!マクロを使うとproc_macro::TokenStreamではなくproc_macro2::TokenStreamが生成されます。proc_macro2::TokenStreamproc_macro2クレートによって提供されるトークン列です。proc_macro::TokenStreamが Rust コンパイラの使用するトークン列です。 proc_macro2::TokenStreamproc_macro::TokenStream::fromproc_macro::TokenStreamに変換できるので、以下のようにして生成したトークン列を返します。

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let _input = parse_macro_input!(input as DeriveInput);

    let expand = quote! {
        impl Command {
            pub fn builder() {}
        }
    };
    proc_macro::TokenStream::from(expand)
}

この段階でのマクロの実装は以下のようになります。

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let _input = parse_macro_input!(input as DeriveInput);

    let expand = quote! {
        impl Command {
            pub fn builder() {}
        }
    };
    proc_macro::TokenStream::from(expand)
}
CommandBuilderを返すようにbuilder関数の実装を変更する

前のステップで実装したbuilder関数は空ですので、このままでは何もしてくれません。次の段階として、ハードコードされたCommandBuilder構造体を返すようにbuilder関数の実装を変更してみましょう。

まずはCommandBuilder構造体の定義をquote!マクロ内に追加します。

let expand = quote! {
    pub struct CommandBuilder {
        executable: Option<String>,
        args: Option<Vec<String>>,
        env: Option<Vec<String>>,
        current_dir: Option<Vec<String>>,
    }
    impl Command {
        pub fn builder() {}
    }
};

次にbuilder関数内からデフォルト値でフィールドを埋めたCommandBuilderを返すように変更します。CommandBuilderのフィールドはすべて Optional なのでNoneで埋めておきます。

let expand = quote! {
        ...略...
    impl Command {
        pub fn builder() -> CommandBuilder {
            CommandBuilder {
                executable: None,
                args: None,
                env: None,
                current_dir: None,
            }
        }
    }
};

最終的な実装は以下のようになります。

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let _input = parse_macro_input!(input as DeriveInput);

    let expand = quote! {
        pub struct CommandBuilder {
            executable: Option<String>,
            args: Option<Vec<String>>,
            env: Option<Vec<String>>,
            current_dir: Option<Vec<String>>,
        }

        impl Command {
            pub fn builder() -> CommandBuilder {
                CommandBuilder {
                    executable: None,
                    args: None,
                    env: None,
                    current_dir: None,
                }
            }
        }
    };
    proc_macro::TokenStream::from(expand)
}
構造体名、フィールド名に応じた Builder 構造体を生成する

さて、ここまででCommand構造体用の Builder 構造体を返すマクロができました。ただ、このままだとほかの構造体に対して使用できません。derive マクロが適用された構造体の名前やフィールドに応じて適した実装を与えるようにしたいです。Builder 構造体の要件をまとめると以下のようになります。

  • 構造体の名前は derive マクロの適用された構造体名の末尾にBuilderを付けたものとする
  • フィールド名は derive マクロの適用された構造体のフィールド名と同じものを使う
  • フィールドの型は derive マクロの適用された構造体のフィールドの型を Optional にしたものとする

このような要件を満たすためには derive マクロが適用された構造体の情報を取得する必要があります。では、どこから取得すれば良いのでしょうか。derive マクロの入力をパースすると以下のようなsyn::DeriveInput構造体が得られるのですが、実はこの構造体の中に必要な情報が入っています。

pub struct DeriveInput {
   pub attrs: Vec<Attribute>,
   pub vis: Visibility,
   pub ident: Ident,
   pub generics: Generics,
   pub data: Data,
}

このステップに関係する部分は以下の 3 つです。

  • visは構造体の可視性の情報を保持しています
  • identは identifier の略で、変数名などの Rust コード中の識別子の情報を保持しています。ここでは derive マクロが付与された構造体/enum の名前を保持しています
  • dataは構造体の保持するフィールドの情報を持っています

構造体名はidentを使えば取得できそうです。derive マクロの使われた構造体名の末尾にBuilderを付けたものを Builder 構造体の名前にしたいので、format_ident!マクロを使って末尾にBuilderを結合します。format_ident!マクロはこのように新たな識別子を作るときに使用します。詳しくはquoteクレートのGitHub リポジトリをご覧ください。

let ident = input.ident;
let builder_name = format_ident!("{}Builder", ident);

構造体名についてはこれで良さそうです。次はフィールド名と型を取得しましょう。フィールドの情報はdataフィールドに入っているのでした。syn::Dataは以下のような enum です。今回扱うのは構造体なのでDataStruct構造体の中身を見てみましょう。

pub enum Data {
    Struct(DataStruct),
    Enum(DataEnum),
    Union(DataUnion),
}

syn::DataStructはこうなっています。fieldsがフィールドの情報を持っています。

pub struct DataStruct {
    pub struct_token: Struct,
    pub fields: Fields,
    pub semi_token: Option<Semi>,
}

Fieldsは以下のような enum です。Namedが名前のついたフィールドです。今回は Unit 構造体やタプル構造体はサポートしないのでNamedだけ見てみましょう。

pub enum Fields {
    Named(FieldsNamed),
    Unnamed(FieldsUnnamed),
    Unit,
}

FieldsNamedはフィールド情報を持っているField構造体を複数保持しており、named.iter()Field構造体のインスタンスをイテレートできます。

pub struct FieldsNamed {
    pub brace_token: Brace,
    pub named: Punctuated<Field, Comma>,
}

syn::Field構造体はこうなっています。identがフィールド名でtyが型の情報を持っています。ようやく必要な情報までたどり着きました。

pub struct Field {
    pub attrs: Vec<Attribute>,
    pub vis: Visibility,
    pub ident: Option<Ident>,
    pub colon_token: Option<Colon>,
    pub ty: Type,
}

パターンマッチを使って必要な情報だけを取り出します。サポートしないケースについては今はpanic!させておきましょう(後のステップでちゃんとコンパイルエラーが出るようにします)。この後で使いやすいようにidenttyVecに格納しておきます。

let (idents, types): (Vec<Ident>, Vec<Type>) = match input.data {
    Data::Struct(data) => match data.fields {
        Fields::Named(fields) => fields
            .named
            .into_iter()
            .map(|field| {
                let ident = field.ident;
                let ty = field.ty;
                (ident.unwrap(), ty)
            })
            .unzip(),
        _ => panic!("no unnamed fields are allowed"),
    },
    _ => panic!("expects struct"),
};

さて、必要なものはすべて準備できました。これらを使ってマクロの出力として返すトークン列を生成しましょう。

この前のステップでquote!マクロについて

Rust のコードと(ほぼ)同じ構文で記述することができ

と説明しましたが、quote!マクロの内部では特殊な記法を使うことができます。たとえば、quote::ToTokensトレイトを実装する変数にquote!マクロの中で#をつけるとその変数の内容が挿入されます。これを使って適切な名前や型のフィールドを持った構造体を動的に生成します。今までに出てきたsyn::Identなどの構造体はquote::ToTokensトレイトを実装しているので特に気にせず使うことができます。

これを使ってCommandBuilder構造体の構造体名を書き直すと以下のようになります。

quote!{
    #vis struct #builder_name {
        executable: Option<String>,
        args: Option<Vec<String>>,
        env: Option<Vec<String>>,
        current_dir: Option<Vec<String>>,
    }
}

また、quote!マクロの中では#(...)*のような形でIntoIteratorを実装した型の変数を繰り返して挿入できます。これを使ってCommandBuilder構造体のフィールド名も書き換えると以下のようになります。identstypesはそれぞれフィールド名と型を格納したベクタですので、以下のようにしてベクタの各値について繰り返し展開できます。)*の間に文字を記述するとその文字で区切って展開してくれるので、以下では#(...),*のようにして構造体のフィールドをベクタから生成しています。

quote!{
    #vis struct #builder_name {
        #(#idents: Option<#types>),*
    }
}

さて、これでハードコードされた構造体名やフィールド名などをすべて取り除くことができました。 同じようにbuilder関数も書き換えると以下のようになります。

quote! {
    #vis struct #builder_name {
       #(#idents: Option<#types>),*
    }

    impl #ident {
        pub fn builder() -> #builder_name {
            #builder_name {
                #(#idents: None),*
            }
        }
    }
}

以上をまとめると最終的なマクロの実装は以下のようになります。

use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident, Type};

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let ident = input.ident;
    let vis = input.vis;

    let builder_name = format_ident!("{}Builder", ident);
    let (idents, types): (Vec<Ident>, Vec<Type>) = match input.data {
        Data::Struct(data) => match data.fields {
            Fields::Named(fields) => fields
                .named
                .into_iter()
                .map(|field| {
                    let ident = field.ident;
                    let ty = field.ty;
                    (ident.unwrap(), ty)
                })
                .unzip(),
            _ => panic!("no unnamed fields are allowed"),
        },
        _ => panic!("expects struct"),
    };

    let expand = quote! {
        #vis struct #builder_name {
           #(#idents: Option<#types>),*
        }

        impl #ident {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(#idents: None),*
                }
            }
        }
    };
    proc_macro::TokenStream::from(expand)
}

03-call-setters

目標

  • CommandBuilder構造体の各フィールドに対する setter メソッドを作る

実装方針

具体的な実装から考えるとやりやすいです。Command関数のexecutableフィールドを例にとると setter は以下のようになるので、これを前のステップと同様、変数の展開や繰り返しを使って書き直しましょう。

pub fn executable(&mut self, executable: String) -> &mut Self {
    self.executable = executable
    self
}

実装

実装方針で書いた具体的なフィールドの実装を書き直しましょう。変数の展開や繰り返しの書き方はすでに説明した通りです。#(...)*の括弧内には変数以外を入れることもできます。今回は関数の実装自体を#(...)*で繰り返してやれば良いでしょう。

impl #builder_name {
    #(pub fn #idents(&mut self, #idents: #types) -> &mut Self {
        self.#idents = Some(#idents);
        self
    })*
}

最終的な実装は以下の通りです。

use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident, Type};

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let ident = input.ident;
    let vis = input.vis;

    let builder_name = format_ident!("{}Builder", ident);
    let (idents, types): (Vec<Ident>, Vec<Type>) = match input.data {
        Data::Struct(data) => match data.fields {
            Fields::Named(fields) => fields
                .named
                .into_iter()
                .map(|field| {
                    let ident = field.ident;
                    let ty = field.ty;
                    (ident.unwrap(), ty)
                })
                .unzip(),
            _ => panic!("no unnamed fields are allowed"),
        },
        _ => panic!("expects struct"),
    };

    let expand = quote! {
        #vis struct #builder_name {
           #(#idents: Option<#types>),*
        }

        impl #builder_name {
            #(pub fn #idents(&mut self, #idents: #types) -> &mut Self {
                self.#idents = Some(#idents);
                self
            })*
        }

        impl #ident {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(#idents: None),*
                }
            }
        }
    };
    proc_macro::TokenStream::from(expand)
}

04-call-build

目標

  • Command構造体のインスタンスを返すbuildメソッドをCommandBuilder構造体に実装する
    • CommandBuilderの各フィールドがNoneの場合はエラーを返すようにする

実装方針

これも具体的な実装から考えるとわかりやすいです。Command構造体の場合は以下のようになるので、これを変数の展開や繰り返しを使って書き直しましょう。

impl CommandBuilder {
    ...略...
    pub fn build() -> Result<Command, Box<dyn Error>> {
        if self.executable.is_none() {
            return Err(...略...)
        }
        ...略...
        if self.current_dir.is_none() {
            return Err(...略...)
        }
        Command {
            executable: self.executable.clone().unwrap(),
            args: self.args.clone().unwrap(),
            env: self.env.clone().unwrap(),
            current_dir: self.current_dir.clone().unwrap(),
        }
    }
 }

実装

ガード節をまず作ります。このようにquote!を使って部分的にトークン列を作って後で組み合わせることもできます。

let checks = idents.iter().map(|ident| {
    let err = format!("Required field '{}' is missing", ident.to_string());
    quote! {
        if self.#ident.is_none() {
            return Err(#err.into())
        }
    }
});

前のステップと同じようにしてbuild関数を作ります。上で作ったガード節を#(#checks)*で展開しています。

let expand = quote! {
    ...略...
    pub fn build(&mut self) -> Result<#ident, Box<dyn std::error::Error>> {
        #(#checks)*
        Ok(#ident {
            #(#idents: self.#idents.clone().unwrap()),*
        })
    }
    ...略...
}

最終的な実装は以下の通りです。

use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident, Type};

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let ident = input.ident;
    let vis = input.vis;

    let builder_name = format_ident!("{}Builder", ident);
    let (idents, types): (Vec<Ident>, Vec<Type>) = match input.data {
        Data::Struct(data) => match data.fields {
            Fields::Named(fields) => fields
                .named
                .into_iter()
                .map(|field| {
                    let ident = field.ident;
                    let ty = field.ty;
                    (ident.unwrap(), ty)
                })
                .unzip(),
            _ => panic!("no unnamed fields are allowed"),
        },
        _ => panic!("expects struct"),
    };

    let checks = idents.iter().map(|ident| {
        let err = format!("Required field '{}' is missing", ident.to_string());
        quote! {
            if self.#ident.is_none() {
                return Err(#err.into())
            }
        }
    });

    let expand = quote! {
        #vis struct #builder_name {
           #(#idents: Option<#types>),*
        }

        impl #builder_name {
            #(pub fn #idents(&mut self, #idents: #types) -> &mut Self {
                self.#idents = Some(#idents);
                self
            })*

            pub fn build(&mut self) -> Result<#ident, Box<dyn std::error::Error>> {
                #(#checks)*
                Ok(#ident {
                    #(#idents: self.#idents.clone().unwrap()),*
                })
            }
        }

        impl #ident {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(#idents: None),*
                }
            }
        }
    };
    proc_macro::TokenStream::from(expand)
}

05-method-chaining

目標

  • CommandBuilder構造体でメソッドチェーンを使えるようにする

実装方針

03-call-setters ステップで実装した setter は&mut Selfを返すようになっているので、実はすでにメソッドチェーンを使えるようになっています。そのため、このステップでは追加の実装をする必要はありません。

実装

このステップでは追加の実装は必要ありません。このステップ用のテストを実行して変更なしでテストをパスすることをチェックしてみましょう。

まとめ

前編では Builder パターンを実現するのに最低限必要な機能を持ったマクロを作りました。 しかし、まだ Optional なフィールドの取り扱いに不十分な点があったり、ベクタ型のフィールドの取り扱いに改善の余地があったりします。後編ではそのあたりの機能を実装していきます。

後編へ続く。

参考文献