(雑な導入)
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行/型なので許容範囲だとは思いますが。