はじめに
こんにちは。バックエンドエンジニアのよしかわです。本記事では Rust の簡単なマクロの実装例をご紹介します。
この記事はエモーションテック Advent Calendar 2024の19日目の記事です。
動機
まず今回紹介するマクロを実装した動機について述べます。弊社では下記のようなレイヤードアーキテクチャを採用しているのですが、レイヤーをまたぐ際の値の詰め替えについての気掛かりが動機となっています。
project/ |- api/ |- domain/ |- infrastructure/ |- usecase/ |- Cargo.toml
具体例で説明します。下記のような構造体 FooValue
が domain レイヤーに定義されていたとします。numbers
と strings
は非公開で getter を通じてアクセスするようになっています。これは numbers
と strings
の長さが同じでなければならないといった不変条件を守りたい状況を想定しています。
pub struct FooValue { numbers: Vec<usize>, strings: Vec<String>, } impl FooValue { pub fn get_numbers(&self) -> &[usize] { &self.numbers } pub fn get_strings(&self) -> &[String] { &self.strings } }
このとき FooValue
をそのまま JSON にして返したいとすると、下記のような構造体 FooResponse
を api レイヤーに定義するのが素直だと思います。
#[derive(Serialize)] pub struct FooResponse { pub numbers: Vec<usize>, pub strings: Vec<String>, }
気掛かりは FooValue
から FooResponse
に値を詰め替えるところにあります。 FooValue
の所有権を持っていたとしても詰め替えの実装に clone
が必要となってしまっています。 numbers
や strings
が非公開だからです。
impl FooResponse { pub fn from(foo: FooValue) -> Self { Self { numbers: foo.get_numbers().clone(), strings: foo.get_strings().clone(), } } }
実際に性能問題が生じることは多くないとしても、レイヤー構造にしたことで余分な clone
が求められるというのは少し損に感じます。以下のように構造体をばらすようなメソッドを用意すれば clone
を避けられるものの構造体やフィールドの数が多くなってくると煩雑です。
impl FooValue { pub fn into_parts(self) -> (Vec<usize>, Vec<String>) { (self.numbers, self.strings) } }
impl FooResponse { pub fn from(foo: FooValue) -> Self { let (numbers, strings) = foo.into_parts(); Self { numbers, strings, } } }
実装
「構造体をばらすようなメソッド」は手で書くと煩雑ですが、機械的に定義できるのでマクロで実装できそうです。そこで下記のような展開を行うマクロを考えてみます。
#[derive(Parts)] pub struct FooValue { numbers: Vec<usize>, strings: Vec<String>, }
↓
pub struct FooValueParts { pub numbers: Vec<usize>, pub strings: Vec<String>, } impl FooValue { pub fn into_parts(self) -> FooValueParts { FooValueParts { numbers: self.numbers, strings: self.strings, } } }
FooValue
の定義を切り貼りすれば FooValueParts
も into_parts
も簡単に組み立てられそうなのが見て取れるかと思います。実際このマクロは50行ほどで実装できてしまいます。マクロの実装をしたことがなくても末尾の TokenStream::from(quote! {
... })
から逆に辿れば何をやっているか何となく分かるのではないでしょうか。
use proc_macro::TokenStream; use quote::{format_ident, quote}; use syn::spanned::Spanned; use syn::{parse_macro_input, Data, DeriveInput, Error, Fields}; #[proc_macro_derive(Parts)] pub fn derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let span = input.span(); let unsupported = |types| Error::new(span, format!("Parts is unsupported for {types}")).to_compile_error(); let vis = &input.vis; let original_struct_name = input.ident; let parts_struct_name = format_ident!("{original_struct_name}Parts"); let mut parts_field_defs = vec![]; let mut parts_field_inits = vec![]; match &input.data { Data::Struct(data) => match &data.fields { Fields::Named(fields) => { for field in &fields.named { let field_name = &field.ident; let ty = &field.ty; parts_field_defs.push(quote! { pub #field_name: #ty, }); parts_field_inits.push(quote! { #field_name: self.#field_name, }); } } Fields::Unnamed(_) => return unsupported("tuple structs").into(), Fields::Unit => return unsupported("unit structs").into(), }, Data::Enum(_) => return unsupported("enums").into(), Data::Union(_) => return unsupported("unions").into(), }; TokenStream::from(quote! { #vis struct #parts_struct_name { #(#parts_field_defs)* } impl #original_struct_name { pub fn into_parts(self) -> #parts_struct_name { #parts_struct_name { #(#parts_field_inits)* } } } }) }
このように実装したマクロを使って FooValueParts
や into_parts
を定義してしまえば、あとは下記のような詰め替えを書くだけです。clone
も必要ありません。
impl FooResponse { pub fn from(foo: FooValue) -> Self { let parts = foo.into_parts(); Self { numbers: parts.numbers, strings: parts.strings, } } }
おわりに
今回は Rust の簡単なマクロの実装例をご紹介しました。得られるメリットがマクロを使うことによる複雑さに見合うかはプロジェクト次第かと思います。clone
のコストを気にしなくてよい場合も多いでしょうし、詰め替え自体をなくす方向を探る方が良い場合もあるかもしれません。ただこういう選択肢を持ってみるのも面白いのではないかと思います。
エモーションテックでは顧客体験・従業員体験の改善をサポートし世の中の体験を変えるプロダクトの開発に Rust を利用しております。もし興味を持っていただけましたら、ぜひ採用ページからご応募をお願いいたします。