(雑な導入)
rust、なんかtraitをちょっと賢くimplしようとすると衝突しがちです。 でもあきらめないで! 場合によっては回避できるかもしれません。
問題
ある商品をお買い物にいくtrait、GoToBuyなるものをこんな感じで定義します:
trait GoToBuy {
fn go_to_buy();
}
買う商品は、こんな感じでいくつか定義しましょうか:
struct Apple; // りんご
struct Banana; // ばなな
struct Cherries; // さくらんぼ
struct Cat; // ねこ
struct Dog; // いぬ
いちいち各商品ごとにGoToBuyを実装してもいいのですが、果物は果物屋さんに、動物はペット屋さんに行くという共通点があるので、実装をまとめたいですよね。 果物と動物のtraitを定義してやりましょう:
trait FruitTrait {}
impl FruitTrait for Apple {}
impl FruitTrait for Banana {}
impl FruitTrait for Cherries {}
trait AnimalTrait {}
impl AnimalTrait for Cat {}
impl AnimalTrait for Dog {}
そして、それぞれのtraitごとにGoToBuyを定義してやれば完璧ですね:
impl<T: FruitTrait> GoToBuy for T {
fn go_to_buy() {
println!("果物屋さんにGo!");
}
}
impl<T: AnimalTrait> GoToBuy for T {
fn go_to_buy() {
println!("ペット屋さんにGo!");
}
}
しかし残念ながら、これは通りません:
error[E0119]: conflicting implementations of trait `GoToBuy`
--> src\main.rs:26:1
|
20 | impl<T: FruitTrait> GoToBuy for T {
| ----------------------------- first implementation here
...
26 | impl<T: AnimalTrait> GoToBuy for T {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation
For more information about this error, try `rustc --explain E0119`.
実装が衝突していると怒られてしまいます。 フルーツと動物なんだから衝突してないだろという気もしますが、未来の世界でフルーツ獣とかが登場したら衝突してしまうので、仕方ないですね。 もうちょっと真面目に書くと、コンパイラ側からはFruitTraitとAnimalTraitが同時に実装されている型が存在しえないことがわからないので、このように怒られてしまっています。
でも仕方ないで済ませたくないじゃないですか。 今回はアホみたいな1行implだからいいですが、もしかしたら100行ある巨大implかもしれないですし、フルーツと動物がそれぞれ100種類いるかもしれません。 フルーツ獣は登場しないという前提の下で、これをなんとか実装できないでしょうか? というのが今回の問題です。
ちなみに、どんな組み合わせだと衝突すると判定され、どんな組み合わせだと衝突しないかはRFC 1210に詳細な例が載っています。 暇なときにでも読んでみてください。
解決策その1:型をいじる
さっきの例だと、どっちもimpl ... for Tと、型Tに対してimplをしていました。 Tはなんでもあり型なので、うかつにimpl対象に使うと衝突しがちです。 代わりに、こんな感じにしてみてはどうでしょうか:
struct Fruit<T>(T);
struct Animal<T>(T);
struct AppleTag;
struct BananaTag;
struct CherriesTag;
struct CatTag;
struct DogTag;
type Apple = Fruit<AppleTag>;
type Banana = Fruit<BananaTag>;
type Cherries = Fruit<CherriesTag>;
type Cat = Animal<CatTag>;
type Dog = Animal<DogTag>;
impl<T> GoToBuy for Fruit<T> {
fn go_to_buy() {
println!("果物屋さんにGo!");
}
}
impl<T> GoToBuy for Animal<T> {
fn go_to_buy() {
println!("ペット屋さんにGo!");
}
}
こうすれば、ふたつのimplは互いに違う型、Fruit<T>とAnimal<T>に対して行っているので、衝突のしようがなくなりコンパイルが通るようになります。 やったね。
この方法の問題は、もちろんAppleやCatなどの型をいじる必要があったことです。 このケースではこれらの型は何もデータを持っていないハリボテなので好き勝手できましたが、他crateから持ってきた型だったり中身がある型だったりするとなかなかいじるのは難しいことがあります。 運がよければここで言うFruit/Animalの型にDeref/DerefMutを実装するだけでなんとかなることもありますが、そうでないケースも多々あります。
解決策その2: traitをもう一段かます
こんどは型ではなくtraitの側をいじるアイディアです。 いったんさっきの解決策のコードは忘れて、また一からGoToBuyと各種フルーツ動物を定義します:
trait GoToBuy {
fn go_to_buy();
}
struct Apple;
struct Banana;
struct Cherries;
struct Cat;
struct Dog;
そして、FruitTagとAnimalTagという型を定義します。 名前にTagとついている通り、これらの型はただの印であって中身はなにもありません:
struct FruitTag;
struct AnimalTag;
そしてそして、GoToBuyの変種、GoToBuyImplというtraitを定義します。 この新しいtraitは、型引数を取るというのがポイントです。 ここの型引数にはさっきのTag二種が入ります。 幸いstructの型引数と違い、traitの型引数は中で一切使わなくてもコンパイラに怒られたりはしませんので、型引数は一切内部で利用しません。
そして、果物屋さんかペット屋さんに行くコードを、それぞれimpl GoToBuyImpl<FruitTag>とimpl GoToBuyImpl<AnimalTag>で記述します。
この型引数を与えられたtraitふたつは違うtrait扱いなので、衝突エラーは発生しません:
trait GoToBuyImpl<ItemTypeTag> {
fn go_to_buy_impl();
}
impl<T> GoToBuyImpl<FruitTag> for T {
fn go_to_buy_impl() {
println!("果物屋さんにGo!");
}
}
impl<T> GoToBuyImpl<AnimalTag> for T {
fn go_to_buy_impl() {
println!("ペット屋さんにGo!");
}
}
まだもう少しコードを書く必要があります。
次に、各種商品に自分がどのTagに対応するのかを設定します。 trait Itemというtraitを作り、関連型をimplさせることで設定できます:
trait Item {
type TypeTag;
}
impl Item for Apple {
type TypeTag = FruitTag;
}
impl Item for Banana {
type TypeTag = FruitTag;
}
impl Item for Cherries {
type TypeTag = FruitTag;
}
impl Item for Cat {
type TypeTag = AnimalTag;
}
impl Item for Dog {
type TypeTag = AnimalTag;
}
そして最後に、GoToBuyからGoToBuyImplへの橋渡しをしてやります。
つまり商品ごとに、対応するTagをItem::TypeTagから取り出し、それをGoToBuyImplに渡してやるというわけです:
impl<T> GoToBuy for T
where
T: Item + GoToBuyImpl<T::TypeTag>,
{
fn go_to_buy() {
T::go_to_buy_impl()
}
}
このimplは、GoToBuyをTに対してimplしている怖いパターンですが、他にGoToBuyのimplは存在しないので衝突は起こりません。 セーフです。
この手法の最大のメリットは、Appleなどの型のほうにはノータッチで済ませられることです。 たぶん実用上ではtraitよりも型の方が自分でいじれないケースが多いと思います(知らんけど)。 デメリットはコードの記述量が長くなりがちなところで、なんか新しいほにゃららimplってtraitとそれへの橋渡しを書かなきゃいけなくなったり、impl Item for Appleとかを何度も書かなきゃいけないことですね。 所詮は3行/型なので許容範囲だとは思いますが。