Rustで T: From<U> のとき From<MyStruct<U>> for MyStruct<T> を実装したい

一応解決済。

概要

Rust (2016-08-23時点でのstableな最新は Rust-1.11.0) で以下のようなことを行いたい。 要するに、型パラメータが変換可能なとき、外側の構造体もまた変換可能であるようにしたい。

        #[derive(Debug, Clone, Copy)]
struct Point<T: Clone + Copy> {
    pub x: T,
    pub y: T,
}

impl<T, U: From<T>> From<Point<T>> for Point<U> {
    fn from(o: Point<T>) -> Self {
        Point {
            x: o.x.into(),
            y: o.y.into(),
        }
    }
}

fn main() {
    let p_i16 = Point::<i16> {
        x: 3,
        y: 4,
    };
    println!("p_i16: {:?}", p_i16);
    let p_f64: Point<f64> = p_i64();
    println!("p_f64: {:?}", p_f64);
}
      
        error: conflicting implementations of trait `std::convert::From<Point<_>>` for type `Point<_>`: [--explain E0119]
 --> <anon>:7:1
  |>
7 |> impl<T, U: From<T>> From<Point<T>> for Point<U> {
  |> ^
note: conflicting implementation in crate `core`

error: aborting due to previous error
      
Rust Playgroundで(Rust-1.11.0 stableで)コンパイルした場合のエラーメッセージ

有難いことに、Rustにおいては厄介事の種となる暗黙の型変換は制約が強い(Deref trait等で実装できるが複数の変換先を用意できない)ので、擬似的なポインタや参照のように透過的な変換を実現したい理由がなければ、From traitInto traitを実装することになる。

これを踏まえたうえで上記コードを説明しよう。

Point<T>型は、2次元の点を表し、その座標をT型で持っているものである。 要求は簡潔明瞭で、T型がU型に変換可能であれば、Point<T>型もまたPoint<U>型に変換可能であれ、ということである。 C++であれば以下のように実装したことだろう。

        #include <iostream>
#include <type_traits>

template <typename T>
struct Point {
    T x;
    T y;

    Point(T x_, T y_)
    :x{x_}, y{y_}
    {}
    // template <typename U, typename=std::enable_if_t<std::is_convertible<T, U>{}>>
    template <typename U>
    Point(const Point<U> &o)
    :x{static_cast<T>(o.x)}, y{static_cast<T>(o.y)}
    {}
};

int main(void) {
    Point<int16_t> p_i16{3, 4};
    Point<double> p_f64 = p_i16;
    std::cout << "p_i16 { " << p_i16.x << ", " << p_i16.y << " }" << std::endl;
    std::cout << "p_f64 { " << p_f64.x << ", " << p_f64.y << " }" << std::endl;
    return 0;
}
      
C++で同様のことをしようとした場合 (Wandboxへのリンク)
        p_i16 { 3, 4 }
p_f64 { 3, 4 }

      
上記C++コードの出力

C++では、RustのFrom traitに対応するのがコンストラクタ、Into traitに対応するのがキャスト演算子と考えていいだろう。 今回はコンストラクタで対応した。

さて本題のRustのコードであるが、エラーの原因にFrom<Point<T>> for Point<U>の実装がcore crateと衝突していることが挙げられている。 これはFromでなくIntoを使ってImpl<T, U: Into<T>> Into<Point<T>> for Point<U>としても同様のエラーになる。 ユーザ定義型に対してまでcore crateで定義されているものといえば、反射律(reflexive law)だ。

結論を言えば、このreflexive lawがエラーの原因だ。 Fromはreflexiveである(すなわち全てのTに対してcore crateでFrom<T> for Tが実装されている)ため、問題のコードでU = Tの場合であるFrom<Point<T>> for Point<T>が実装される。 一方、またもやFromのreflexivityより、Point<T>に対してcore crateでFrom<Point<T>> for Point<T>が実装される。 斯くしてFrom<Point<T>> for Point<T>が二重で実装され、衝突と相成ったわけである。

とりあえずの解決策

mapメソッドを定義する。 Option::mapのようなものだ。

        #[derive(Debug, Clone, Copy)]
struct Point<T: Clone + Copy> {
    pub x: T,
    pub y: T,
}

impl<T: Clone + Copy> Point<T> {
    fn map<U: Clone + Copy, F: Fn(T) -> U>(self, f: F) -> Point<U> {
        Point {
            x: f(self.x),
            y: f(self.y),
        }
    }
}

fn main() {
    let p_i16 = Point::<i16> {
        x: 3,
        y: 4,
    };
    println!("p_i16: {:?}", p_i16);
    let p_f64: Point<f64> = p_i16.map(Into::into);
    println!("p_f64: {:?}", p_f64);
}
      
コンパイルの通るバージョン (Rust Playgroundへのリンク)
        p_i16: Point { x: 3, y: 4 }
p_f64: Point { x: 3, y: 4 }

      
実行結果

どうにかしてFromIntoを実装したかったが、無理そうな雰囲気があったので妥協した。 もしかすると、オーバーロード関連の機能の具合によってはFromなどで実装できるようになるのかもしれないが、今のところstableでは使えないので、せめてstdにあるOptionResultとインターフェースを合わせて実装してしまうことにした。 foo.map(|v| v.into())よりもfoo.map(Into::into)の方が、2文字短くなるし意図もわかりやすいというのがポイントだ。

追記 (2017/03/18): 解決策2

良い方法が紹介されている記事があった: Math with distances in Rust: safety and correctness across units

        #[derive(Debug, Clone, Copy)]
struct Point<T: Clone + Copy> {
    pub x: T,
    pub y: T,
}

impl<'a, T, U> From<&'a Point<T>> for Point<U>
    where T: 'a + Clone + Copy,
          U: Clone + Copy + From<T>,
{
    fn from(o: &'a Point<T>) -> Self {
        Point {
            x: o.x.into(),
            y: o.y.into(),
        }
    }
}

fn main() {
    let p_i16 = Point::<i16> {
        x: 3,
        y: 4,
    };
    println!("p_i16: {:?}", p_i16);
    let p_f64: Point<f64> = (&p_i16).into();
    println!("p_f64: {:?}", p_f64);
    let p_f32 = Point::<f32>::from(&p_i16);
    println!("p_f32: {:?}", p_f32);
}
      
コンパイルの通るバージョンその2 (Rust Playgroundへのリンク)

T&T は別の型として扱われるから、これを使い分ければ core crate での reflexivity law の実装と競合しないということである。 参照を使う都合 move ができなくなるため、実装によっては素朴な実装より効率が悪くなるかもしれないが、こうして内部の型を変換したいという場合の多くは数値やら copy 可能な単純な型だろうから、そうした場合にはオーバーヘッドもなくユーザにとっても煩雑でない、効果的な方法であるといえるだろう。