Rustでtraitのassociated typeに対してtrait boundaryを課す

RustのTraitは内部に型を持つことがある。関連型(associated type)だ

trait Hoge {
    type Value;
}

impl Hoge for Piyo {
    type Value = Fuga;
}

このValue型についてトレイト境界を付けたい。 つまり、ある型Tがあり、型<T as Hoge>::ValueOtherTraitを満たす場合のみ使える関数を使いたい。

以下のように書けたらよいのだが。

fn foo<T: Hoge<Value: OtherTrait>>() -> ()
{
    // ...
}

残念ながらこれは通らない。

error: expected one of `!`, `(`, `+`, `,`, `::`, `<`, or `>`, found `:`
  --> src/main.rs:11:21
   |
11 | fn foo<T: Hoge<Value: OtherTrait>>() -> () {
   |                     ^ expected one of 7 possible tokens here

ではどうやって書くかというと、実は割と素直に書くと通る。

fn foo<T>(x: T) -> ()
where
    T: Hoge, // ここではまだ何も言わない
    <T as Hoge>::Value: OtherTrait // これを足す
{
    println!("hello! {}", x.value().other());
}

トレイト境界の左辺にそれ書けるんかい! と思った。

動くサンプルを置いておく。 play.rust-lang.org

さて、似たような、だが少し違うケースがある。今日実際に困っていたのはこちらだ。

まず、自分のクレートに既にcrate::error::Errorを定義してあり、よくある型についてFrom for Errorを実装しているとしよう。

pub struct Error {/* ... */};
type Result<T> = std::result::Result<T, Error>;

impl From<std::num::ParseIntError> for Error {
    fn from(error :std::num::ParseIntError) -> Error {
        // ...
    }
}

で、何らかのフィールドを持つジェネリクス構造体があるとする。それを読み込めるようにしたい。

pub struct Hoge<T> {/* ... */};

impl<T> std::str::FromStr for Hoge<T> {
    type Err = Error;
    fn from_str(line: &str) -> Result<Self> {
        // ...
    }
}

このfrom_strHoge<T>のフィールド(T型)を読むためにparseを使いたい。そのためには、まずTFromStrを実装している必要がある。

impl<T> std::str::FromStr for Hoge<T>
where
    T: std::str::FromStr
{
    type Err = Error;
    fn from_str(line: &str) -> Result<Self> {
        // ...
    }
}

それだけではない。rustcはすぐに、<T as FromStr>::Errcrate::error::Errorに変換可能でないとこの関数はコンパイルできなくなることを見抜く。なので、ここまで話してきたような、関連型に対するトレイト境界が必要になるのだ。

ところで、今回はFrom</*...*/> for Errorを実装していたのだった。とすると、これはErrorに対して実装されているので、先程までと同じような書き方はできない。 Errorがトレイト境界From<T as FromStr>::Errを満たしている必要があるからだ。 これも、そのまんま書くと通る。

impl<T> std::str::FromStr for Hoge<T>
where
    T: std::str::FromStr,
    Error: From<<T as std::str::FromStr>::Err> // これ
{
    type Err = Error;
    fn from_str(line: &str) -> Result<Self> {
        // ...
    }
}

それ左辺に書けるの!?(驚愕)

Errorは普通に定義されたただのstructだったので、それに対してトレイト境界を設けられるとは思っていなかった。トレイト境界はジェネリクスの型引数に対して課すものだと思っていたからだ。

まあでも、わかってしまえば結構そのまんまだった。試してみるものだ。


追記(1/22)

当初std::ops::Addの実装をよくわかっておらず、ジェネリクストレイトの型パラメータと関連型を取り違えていた(該当箇所は混乱を避けるため消した)。

std::ops::Addの実装は以下のようになっている。

pub trait Add<RHS = Self /* 型パラメータ */ > {
    type Output; // 関連型
    fn add(self, rhs: RHS) -> Self::Output;
}

RHSはあくまで型パラメータであり、Outputが関連型だ。RHSを書くべき場所に関連型を書けないのは当たり前だし、型パラメータにさらなる型パラメータとそのトレイト境界を書けないのも当然だ。