serdeの `derive` feature と名前空間の困った関係

最近のserdeには `derive` というfeatureがあります。これは互換性の上で厄介だという話をします。

## 両方インポートする方式

serdeを使うときは、以下のように書くことが多かったかと思います:

```toml:Cargo.toml
[dependencies]
serde = "1.0.97"
serde_derive = "1.0.97"
```

```rust:main.rs
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を使う方式

一方、以下のように書くこともできます:

```toml:Cargo.toml
[dependencies]
serde = { version = "1.0.97", features = ["derive"] }
```

```rust:main.rs
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から存在する機能](https://github.com/serde-rs/serde/commit/13886435a600bd049e4e9db6e22fac6a0113784a)のようです。ただserdeのドキュメントに書かれているサンプルがこの方式に書き換えられたのは割と最近のようです。

## `derive` の困ったところ

さてこの `derive` ですが、[ライブラリ間の相互運用性で問題が指摘されています](https://github.com/serde-rs/serde/issues/1441)。

まず、以下のコードは `serde` の `derive` featureが**無効なときだけ**コンパイルが通ります。

```rust
// コード1: derive featureが無効なときだけ動く
use serde::{Deserialize, Serialize};
use serde_derive::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct Foo {}
```

一方、以下のコードは `serde` の `derive` featureが**有効なときだけ**コンパイルが通ります。

```rust
// コード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)が同じなら統合される。そうでなければ、どちらも無効。
- 単独インポート同士の場合、衝突する。

単独インポートは、実際には複数名前空間からのインポートにも対応しています。その場合は可能な名前空間全てからインポートしますが、上記の「単独インポート同士の場合、衝突する。」という規則はそのままです。つまり、

```rust
use serde::{Deserialize, Serialize};
use serde_derive::{Deserialize, Serialize};
```

と書いた場合、次のようになります。

- `derive` feature が無効な場合: 1行目は型名前空間、2行目はマクロ名前空間からインポートする。衝突はせず合流する。
- `derive` feature が有効な場合: 1行目は型名前空間+マクロ名前空間、2行目はマクロ名前空間からインポートする。同じ名前空間の同じ名前を複数回単独インポートしようとしているので、衝突する。

一方、 `serde` からのインポートだけを書いた場合、 `derive` featureが無効だったらマクロがインポートされなくなってしまいます。つまり、この2つはどちらも、 `derive` featureに対してポータブルではないということです。

(名前解決の挙動について詳しくは[拙ブログに詳しくまとめました](https://qnighy.hatenablog.com/entry/2019/05/06/190000)ので興味があればどうぞ)

## 完全にポータブルに書く方法

完全にポータブルに書くにはいくつかの方法があります。一番手っ取り早くて堅牢なのは別名インポートすることです。

```rust
use serde::{Deserialize, Serialize};
use serde_derive::{Deserialize as De, Serialize as Ser};

#[derive(Ser, De)]
pub struct Foo {}
```

`serde_derive` 側をglob importにしてもそこそこうまく機能します。ただこれは、将来deriveが増えたときに余計なものまでインポートするかもしれません。

```rust
use serde::{Deserialize, Serialize};
#[allow(unused_imports)]
use serde_derive::*;
```

一応、以下のように書けば `Deserialize`/`Serialize` だけうまく合流させることができるはずです。

```rust
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 を使っていって問題ないはず