7
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?

More than 1 year has passed since last update.

【Rust】タプルへのトレイト実装、マクロが1行でやってくれます。

Last updated at Posted at 2022-03-29

はじめに

タプルへのトレイトの実装はメンバの数ごとに実装を書くことがほとんどだと思われます。よくピラミッドのようになっているコードを見かける気がします。
マクロを用いて一部の記述を省略するものはよく見かけますが、例えば「20という数字だけで、メンバ数20までのタプルに一気に実装する」という方法は見かけないように思えます。

seq-macropasteクレートを用いて実現してみました。

コード①

型パラメータを使用しない場合

Cargo.toml
[dependencies]
seq-macro = "0.3.0"
paste = "1.0.7"
main.rs
use seq_macro::seq;
use paste::paste;

trait MyTrait {
    fn func() {
        println!("func");
    }
}

macro_rules! impls {
    ( $( $t:tt )* ) => {
        impl <$($t)*> MyTrait for ($($t)*) {
        }
    }
}

macro_rules! impls_tuple {
    ($n:expr) => {
        seq!(N in 0..$n {
            paste! {
                impls!( #( [<T~N>], )* );
            }
        });
    };
}

macro_rules! impls_tuple_for {
    ($n:expr) => {
        seq!(N in 1..=$n {
            impls_tuple!(N);
        });
    }
}

impls_tuple_for!(5);

fn main() {
    <(i8,) as MyTrait>::func(); // => func
    <(i8, i16) as MyTrait>::func(); // => func
    <(i8, i16, i32) as MyTrait>::func(); // => func
    <(i8, i16, i32, i64) as MyTrait>::func(); // => func
    <(i8, i16, i32, i64, isize) as MyTrait>::func(); // => func
}

コード②

型パラメータを使用する( <T as Trait>::func() のような記述をする )場合

わかりやすいようにコード①との差分を表示しています。

main.rs
  use seq_macro::seq;
  use paste::paste;

+ trait One {
+     fn one() -> i32 { 1 }
+ }
+
+ // `<T as One>::one()` という記述が可能になる。
+ impl<T> One for T {}

  trait MyTrait {
      fn func() {
          println!("func");
      }
  }

  macro_rules! impls {
-     ( $( $t:tt )* ) => {
-         impl <$($t)*> MyTrait for ($($t)*) {
+     ( $( $t:tt $comma:tt )* ) => {
+         impl <$($t $comma)*> MyTrait for ($($t $comma)*) {
+             fn func() {
+                 println!(
+                     "{:?}", // => (1,) or (1, ..., 1)
+                     (
+                         $(
+                             <$t as One>::one(),
+                         )*
+                     ),
+                 );
+             }
          }
      }
  }

  macro_rules! impls_tuple {
      ($n:expr) => {
          seq!(N in 0..$n {
              paste! {
                  impls!( #( [<T~N>], )* );
              }
          });
      };
  }

  macro_rules! impls_tuple_for {
      ($n:expr) => {
          seq!(N in 1..=$n {
              impls_tuple!(N);
          });
      }
  }

  impls_tuple_for!(5);

  fn main() {
      <(i8,) as MyTrait>::func(); // => (1,)
      <(i8, i16) as MyTrait>::func(); // => (1, 1)
      <(i8, i16, i32) as MyTrait>::func(); // => (1, 1, 1)
      <(i8, i16, i32, i64) as MyTrait>::func(); // => (1, 1, 1, 1)
      <(i8, i16, i32, i64, isize) as MyTrait>::func(); // => (1, 1, 1, 1, 1)
  }

マクロの説明

impls_tuple!

macro_rules! impls_tuple {
    ($n:expr) => {
        seq!(N in 0..$n {
            paste! {
                impls!( #( [<T~N>], )* );
            }
        });
    };
}

seq! マクロにより、第2引数のブロックが 0..$n のレンジ分だけループします(変数 N 使用可能)。ブロック内で #( ... )* と記述することで、その部分に限定してコードを繰り返すことができます。それを記述しない場合は、ブロック全体を繰り返します。

paste! マクロのブロック内では [< ... >] と記述すると、変数を結合して新たな識別子を生成することができます。ただしここで作成しているのは「トークン」です。

この部分で作成されているのは以下のコードになります。

impls!( T0, T1, T2, ... );

またこのマクロを展開している部分も seq! マクロを使用してループしているため、全体としては以下のようにコードが生成されています。

impls!( T0, );
impls!( T0, T1, );
impls!( T0, T1, T2, );
impls!( T0, T1, T2, T3, );
impls!( T0, T1, T2, T3, T4, );
impls!( T0, T1, T2, T3, T4, T5, );

impls!

macro_rules! impls {
    ( $( $t:tt )* ) => {
        impl <$($t)*> MyTrait for ($($t)*) {
        }
    }
}

tt とは「トークン」型です。このマクロは複数のトークンを受け取ります。

トークンとは、コードを「意味の変わらない部分まで区切ったもの」の一つ一つです。国語の文節分けでいう「単語」がコードでいうトークンにあたります。
impl といったキーワード、変数や構造体名のような識別子、 ,(カンマ)や括弧はすべてトークンです。

さて、このマクロに渡していたものは何だったでしょうか?

例えば T0, T1, をトークンに分解すると、T0(識別子)、,(カンマ)、T1(識別子)、,(カンマ) となります。これは後述する「コード②との違い」で重要になります。

このマクロの定義だと、$($t)* と記述すると「引数をそのまま展開する」という挙動になります。よって impls!(T0, T1,) は以下のようなコードを展開します。

impl <T0, T1,> MyTrait for (T0, T1,) {
}

まさにタプルへの実装ですね。
これが、メンバが3の場合、メンバが4つの場合、...と繰り返されることでまとめてタプルへの実装ができるようになります。

また末尾にカンマがあることで、メンバが1つのタプルにも対応しています。

コード①とコード②の違いについて

  macro_rules! impls {
-     ( $( $t:tt )* ) => {
-         impl <$($t)*> MyTrait for ($($t)*) {
+     ( $( $t:tt $comma:tt )* ) => {
+         impl <$($t $comma)*> MyTrait for ($($t $comma)*) {

では最後に、型パラメータを使用する場合と使用しない場合でコードが変わるのかについて解説します。

正確にはどちらも②のコードで動かすことができます。
①のコードは「引数がそのまま展開される」という点でわかりやすいコードではありますが、これから説明することはどちらを使うにしても理解しておくべきです。

先ほど T0(識別子) と ,(カンマ) がどちらもトークンであると説明しました。①のコードでは、マクロは「『トークンが1つ』というパターンが4回繰り返されている」と解釈しています。
②のコードはそうではなく、T0, T1, を「『トークンが2つ』というパターンが2回繰り返されている」という解釈をしています。『トークンが2つ』のそれぞれに $t$comma という変数を割り当てています。これにより識別子を分離して扱うことができます。

②のコードで記述した <$t as One>::one() という関数呼び出しですが、コード①のマクロ定義だった場合次のようなコードが展開されてしまいます。

<T0 as One>::one() // ok
<, as One>::one()  // <= ?
<T1 as One>::one() // ok
<, as One>::one()  // <= ?

それを回避するために、「型パラメータ+カンマ」の繰り返しであることを利用して解釈の仕方を変える方法を用いています。

おわりに

このマクロを実装するメリット

  • 実装するタプルの数が増えたときの対応が安易になった。
  • なんかすごそうなものができた。

このマクロを実装するデメリット

  • より一層わかりづらいマクロを用いることになった。
  • そこまでコードの記述量は減っていないかも

最初に挙げた「20という数字だけで、メンバ数20までのタプルに一気に実装する」マクロは完成こそしましたが、完成させたうえで思うのが、「誰もやっていない・知らないのではなく、そこまでするメリットが薄い」のだと思いました。まあタプル以外にも使い道はありそうですし覚えておいて損はないのではないかな...と思います。

追記

最近作成したクレートで実際に使用してみました。

7
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
7
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?