🌸

Deriving via for Rust

2023/03/10に公開

どうも、みたまです

……春。
桜咲き乱れる出会いと別れのその季節。
Rust で Haskell の DerivingVia のようなことができるライブラリを作りました。
https://github.com/LoliGothick/deriving_via

……お願いします。
お願いします、どうかリポジトリにスターをつけてください。

普通の使い方

普通の derive マクロのように使うこともできます。
ただし普通の derive マクロと違い、DerivingVia では #[deriving] アトリビュートの中に生成したい impl のトレイトを書きます。

#[derive(DerivingVia)]
#[deriving(From, Display)]
pub struct D(i32);

ジェネリクスも使えます。

#[derive(DerivingVia)]
#[deriving(From, Display)]
pub struct D<T: Display>(T);

newtype pattern も使えます。
newtype pattern を使う場合は、#[underlying] をつけることが必須となります。
#[underlying] をつけることで、その型のラッパーとみなされます。
その他のフィールドは Default::default によって初期化できる必要があります。

#[derive(DerivingVia)]
#[deriving(From, Display)]
pub struct Test<T>(#[underlying] i32, std::marker::PhantomData<T>);

Deriving via な使い方

以下のコードを見てください。
AB ともに Display を derive しておりません。
しかし、CDisplay を derive できており、その挙動は i32 と同じです。
なんかすごそうですね。

use deriving_via::DerivingVia;

#[derive(DerivingVia)]
pub struct A(i32);

#[derive(DerivingVia)]
pub struct B(A);

#[derive(DerivingVia)]
#[deriving(Display(via: i32))]
pub struct C(B);

fn main() {
  let c = C(B(A(42)));
  println!("{c}"); // 42
}

よくみると、#[derive(DerivingVia)]AB にもついています。
悲しいことです。

これは、Deriving via のしくみの影響です。

Deriving via のしくみ

Deref の推移的な変換を利用しています。

https://doc.rust-lang.org/reference/type-coercions.html#coercion-types

そのため、#[derive(DerivingVia)] は強制的に Deref トレイトを impl しようとします。
#[derive(DerivingVia)]AB にもついているのはそのためです。

#[derive(DerivingVia)]
#[deriving(Display(via: i32))]
pub struct C(B);

これはつぎのようなコードを生成します。

impl ::core::fmt::Display for C
{
    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
	let de: &i32 = self;
	write!(f, "{}", de)
    }
}

let de: &i32 = self; で推移的な Type Coercion が起こります。
必要なだけ deref が呼ばれ、self&i32 に変換するのです。

#[transitive]

DervingVia は当然 Display 以外も derive することができます。
例えば、Add を derive することができます。
しかし、ここで問題があります。
deref を使って Ci32 にすることはできますが、足し算を行ったあとに C に戻す方法がマクロからはわからないのです。
マクロに戻し方を教えてあげる必要があります。
推移的な型変換をマクロに教える #[transitive] アトリビュートを使います。
各型変換には Into トレイトが要求されるので #[deriving(From)] が必要になります。

use std::fmt::Display;

use deriving_via::DerivingVia;

#[derive(DerivingVia)]
#[deriving(From)]
pub struct A(i32);

#[derive(DerivingVia)]
#[deriving(From)]
pub struct B(A);

#[derive(DerivingVia)]
#[deriving(From, Add(via: i32), Display(via: i32))]
#[transitive(i32 -> A -> B -> C)]
pub struct C(B);

fn main() {
  let c = C(B(A(42))) + C(B(A(42)));
  println!("{c}");
}

実際どう使う?

Value Object に対して Serialize/Deserialize を derive する例。

use deriving_via::DerivingVia;
use serde::{Deserialize, Serialize};
use serde_json::json;

#[derive(Debug, DerivingVia)]
#[deriving(Display, From, Serialize(via: u32), Deserialize(via: u32))]
struct Id<T>(#[underlying] u32, std::marker::PhantomData<T>);

#[derive(Debug, DerivingVia)]
#[deriving(Display, From, FromStr, Serialize(via: String), Deserialize(via: String))]
struct Name<T>(#[underlying] String, std::marker::PhantomData<T>);

#[derive(Debug, Serialize, Deserialize)]
struct User {
    id: Id<User>,
    name: Name<User>,
}

fn main() {
    let user = User {
        id: 1u32.into(),
        name: "mitama".parse().unwrap(),
    };
    let json = json!({ "user": user });
    println!("{json}");
}

Discussion