最近のserdeには derive
というfeatureがあります。これは互換性の上で厄介だという話をします。
両方インポートする方式
serdeを使うときは、以下のように書くことが多かったかと思います:
[dependencies]
serde = "1.0.97"
serde_derive = "1.0.97"
use serde::{Deserialize, Serialize};
use serde_derive::{Deserialize, Serialize};
// 1.29以前の書き方:
// extern crate serde;
// #[macro_use] extern crate serde_derive;
// use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
pub struct Foo {}
// 以下、 Deserialize traitとSerialize traitを使うコード
derive
featureを使う方式
一方、以下のように書くこともできます:
[dependencies]
serde = { version = "1.0.97", features = ["derive"] }
use serde::{Deserialize, Serialize};
// 1.29以前の書き方:
// #[macro_use] extern crate serde;
// use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
pub struct Foo {}
// 以下、 Deserialize traitとSerialize traitを使うコード
これは実はかなり古くて、2017年5月のv0.9.12から存在する機能のようです。ただserdeのドキュメントに書かれているサンプルがこの方式に書き換えられたのは割と最近のようです。
derive
の困ったところ
さてこの derive
ですが、ライブラリ間の相互運用性で問題が指摘されています。
まず、以下のコードは serde
の derive
featureが無効なときだけコンパイルが通ります。
// コード1: derive featureが無効なときだけ動く
use serde::{Deserialize, Serialize};
use serde_derive::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Foo {}
一方、以下のコードは serde
の derive
featureが有効なときだけコンパイルが通ります。
// コード1: derive featureが有効なときだけ動く
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Foo {}
これは名前解決の動作に由来します。Rustではモジュールとは別に名前空間という概念があります。「型・トレイト・モジュール」「値・変数・関数」「マクロ・deriveマクロ・属性」の3つは区別されるので、同じモジュール内に同じ名前で共存できます。(C++ではないC言語の名前空間と似てますね)
-
serde_derive
のルートモジュールには、マクロ名前空間のSerialize
とマクロ名前空間のDeserialize
が定義されています。 -
serde
は、型名前空間のSerialize
と型名前空間のDeserialize
を定義しています。 - それに加えて、
derive
featureが有効なら、serde_derive
に由来するマクロ名前空間のSerialize
/Deserialize
がそこに合流します。
では、同じ名前の use
が複数あったらどうなるでしょうか。Rustの規則は以下のようになっていました。
- 単独インポートとglobインポートであれば、単独インポートが優先される。
- globインポート同士の場合、実体(Def)が同じなら統合される。そうでなければ、どちらも無効。
- 単独インポート同士の場合、衝突する。
単独インポートは、実際には複数名前空間からのインポートにも対応しています。その場合は可能な名前空間全てからインポートしますが、上記の「単独インポート同士の場合、衝突する。」という規則はそのままです。つまり、
use serde::{Deserialize, Serialize};
use serde_derive::{Deserialize, Serialize};
と書いた場合、次のようになります。
-
derive
feature が無効な場合: 1行目は型名前空間、2行目はマクロ名前空間からインポートする。衝突はせず合流する。 -
derive
feature が有効な場合: 1行目は型名前空間+マクロ名前空間、2行目はマクロ名前空間からインポートする。同じ名前空間の同じ名前を複数回単独インポートしようとしているので、衝突する。
一方、 serde
からのインポートだけを書いた場合、 derive
featureが無効だったらマクロがインポートされなくなってしまいます。つまり、この2つはどちらも、 derive
featureに対してポータブルではないということです。
(名前解決の挙動について詳しくは拙ブログに詳しくまとめましたので興味があればどうぞ)
完全にポータブルに書く方法
完全にポータブルに書くにはいくつかの方法があります。一番手っ取り早くて堅牢なのは別名インポートすることです。
use serde::{Deserialize, Serialize};
use serde_derive::{Deserialize as De, Serialize as Ser};
#[derive(Ser, De)]
pub struct Foo {}
serde_derive
側をglob importにしてもそこそこうまく機能します。ただこれは、将来deriveが増えたときに余計なものまでインポートするかもしれません。
use serde::{Deserialize, Serialize};
#[allow(unused_imports)]
use serde_derive::*;
一応、以下のように書けば Deserialize
/Serialize
だけうまく合流させることができるはずです。
use serde::{Deserialize, Serialize};
#[allow(unused_imports)]
use self::__derive_helper::*;
mod __derive_helper {
pub use serde_derive::{Deserialize, Serialize};
}
#[derive(Deserialize, Serialize)]
pub struct Foo {}
derive
を使うべきか、使わざるべきか
完全にポータブルにするには上記のような方法がありますが、基本的には derive
featureを使っていけばいいと思います。
featureはadditiveに解決されます。つまり、ある依存元Aは derive
を必要としていて、別の依存元B は必要としていないとき、 derive
は有効になります。このような性質から、基本的には「derive
が有効になっているとコンパイルが通らないライブラリ」のほうが淘汰されることになるはずです。
もちろん、本来的には、依存される側である serde
側に「derive
が有効なときも、 derive
を必要としていない依存元に対する互換性が壊れない」という性質を守る責務があります。とはいえ、これは名前解決に由来する不可避な現象なので、致し方ないかなと思います。
まとめ
-
serde
にはderive
featureがあり、これを使うと直接serde_derive
に依存しなくてよくなる - featureは本来足し算だが、名前解決の問題で「
derive
が有効になるとコンパイルできない」ということが起こりえる - そのようなライブラリはやがて淘汰されるはずなので、
derive
feature を使っていって問題ないはず