LoginSignup
6
5

More than 1 year has passed since last update.

[Rust] Associated Types と Trait Object

Last updated at Posted at 2022-07-11

以下の駄文は独自研究を多く含んでおり、間違っている可能性は大いにある。

実際の所、後日処理系のコードを読んで確認するつもりである。(そのためのメモという意味合いもある)

Associated Types と Type Parameters

RustのAssociated typesとは、Traitに関連する型を提供する機能である。例えば標準ライブラリのstd::iter::Iteratorの実装は以下のようになっている。

pub trait Iterator {
    type Item;
	// ...
}

ここでIterator::ItemがAssociated typesであり、これはIterator traitが実装される型Tに応じて固有の型<T as Iterator>::Itemを示す。

一方、Type parametersとは、文字通りtraitに対して型パラメータを渡す機能である。例としてはstd::convert::Fromがある。

pub trait From<T> {
    fn from(T) -> Self;
}

これらの違いとしては、Type parameterの場合、型Uに対して複数のFrom<T>を同時に実装することができる(例えば型u32From<bool>From<char>を同時に実装している)
一方、Associated typesでは型Tが決まればAssociated typesも只一通りに決まるのである。

本当か?

...と書いたが、実は例外が存在する。それはTrait objectだ。

Trait objectは、trait MyTraitが与えられた時にdyn MyTraitという型を生成する機能である。すなわち、MyTraitを実装する型を、一定の条件の下(これをObject safetyという)でdyn MyTrait型に型変換することができるのである。

let a = 123_8;
let a = a as dyn Debug;

しかし、実際は上記のコードを実行することはできない。RustではDST(Dynamically Sized Types)を直接扱えないからである。そのため以下のようにBoxを使う。

let a = Box::new(123_i8);
let a = a as Box<dyn Debug>;

この例では、i8型の値をdyn Debug型に型強制しているのである。i8Debugトレイトを実装し、Debugトレイトは Object safe なためこの型変換が許される。

トレイトオブジェクトの何が問題だろうか?

さて、先程以下のように書いた。

一方、Associated typesでは型Tが決まればAssociated typesも只一通りに決まるのである。

では、以下のような(動かない)例を考えてみよう。

trait Tr {
	type X;
}
struct S1;
struct S2;
impl Tr for S1 {
	type X = i8;
}
impl Tr for S2 {
	type X = u8;
}
fn func(_: Box<dyn Tr>) -> <dyn Tr as Tr>::X { ... }

ここで、func()の戻り値型はどうなるだろうか?

  • trait Tr は Associated types Tr::Xを持つ
  • Tが trait Tr を実装している場合、 Associated types Tr::XT に依存して一意に決まる
  • 上記の例でS1, S2, dyn Tr はそれぞれ型であり、trait Trを実装している
  • 実際、<S1 as Tr>::X = i8, <S2 as Tr>::X = u8である
  • では、<dyn Tr as Tr>::Xは何だろうか?

この問題は、Trait objectの「複数の型を一つの型として扱う」という特性とAssociated typesの相性が悪いのが原因である。Rustの解決策は「Trait object に関しては Associated types を Type parameters のように扱う」である。以下、動く例を見てみよう。

そんな単純な話ではない

これでめでたしめでたし...。かと思えばそんなことはない。以下のような例を考えよう。

トレイトが継承されている場合

trait Tr1 {
	type X;
}
trait Tr2: Tr1 {
    type Y;
}

このように継承関係にあるトレイトTr2のトレイトオブジェクトを作りたいと思ったらどうすればよいだろうか?

... 答えはdyn Tr2<X = i8, Y = u8>である。すなわち、継承元のトレイトを含め、すべての”浮いている” Associated types を指定し尽くす必要がある。

Associated type X の値は単純にdyn Tr2<X = i8, Y = u8>::Xとやってもいいし、厳密に<dyn Tr2<X = i8, Y = u8> as Tr1>::Xとやってもいい1

ちなみに以下の例のようにすればXの指定を省略することができる。

trait Tr1 {
	type X;
}
trait Tr2: Tr1<X = i8> {
    type Y;
}

fn func(_: Box<dyn Tr2<Y = u8>>) -> <dyn Tr2<Y = u8> as Tr2>::Y { todo!() }

ここでTr<X = i8>の部分をTr<X = <Self as Tr2>>::Yのようにしたかったが無理であった。

なぜかうまくコンパイルできない例

trait Tr1 {
	type X;
}
trait Tr2: Tr1<X = Self::Y> {
    type Y;
}

fn func(_: Box<dyn Tr2<Y = u8>>) { todo!() }

ここでTr2は間違いなくObject safeであると思うのだが正しくコンパイルできない。これは処理系のバグではないだろうか?

Associated types が trait のメンバの入力もしくは出力として使われる場合

trait Tr {
    type X;
    fn f(self) -> Self::X;
}

fn func(_: Box<dyn Tr<X = i8>>) -> () { todo!() }

実は、上のコードは全く問題なく動作する。では以下の例はどうだろうか?

trait Tr1 {
	type X;
}
trait Tr2: Tr1 {
    type Y;
    fn func1(&self) -> <Self as Tr1>::X;
}

fn func(_: Box<dyn Tr2<X = u8, Y = i8>>) { todo!() }

この場合も全く問題なく動作する。func1()の戻り値の中にSelfが入っているため問題が起きるように見えるが、<Self as Tr1>::XTr1のAssociated types でありながら、実質的には単なる型パラメータとして機能している。そのため、Trait object内での<Self as Tr1>は最早Selfとは関係ないのである。

Where節の扱い

以下の例はどうだろう。

trait Tr1 {
	type X;
}
trait Tr2 {
    fn func1(&self) -> <Self as Tr1>::X where Self: Tr1;
}

fn func(_: Box<dyn Tr2>) { todo!() }

これはコンパイルできない。なぜならSelfのAssociated typesである<Self as Tr1>::Xをメンバの出力として使用しているからである。これはObject safetyに引っかかる。では、以下のようにWhere節でAssociated types Xを指定したらどうなるかというと、やはりこれもObject safetyに引っかかる。おそらく、トレイトオブジェクトのメソッドを呼び出す時には既にSelfの元の型情報は存在しないため、追加の型制約を行うことはできないのだろう。

trait Tr1 {
	type X;
}
trait Tr2 {
    fn func1(&self) -> <Self as Tr1>::X where Self: Tr1<X = u8>;
}

fn func(_: Box<dyn Tr2>) { todo!() }

Object safety

あるトレイトがObject safeである、とは

  • Associated constantsをもたない、かつ
  • Self: Sized ではない、かつ
  • すべてのメンバ関数がObject safeである。

あるメンバ関数がObject safeであるとは

  • Self: Sized 制約を持つ(トレイトオブジェクトでは呼べなくなる2)、または
  • 以下の全てを満たす
    ** 型パラメータを持たない(トレイトオブジェクトに対するメソッドコールの呼び出し先を一意にするため?)、かつ
    ** Selfにdereferenceされるようなレシーバを持つ。
    ** レシーバ以外でSelfを使用しない。(ただしSelfに関連付けられるAssociated typesを使用する場合を除く)

まとめ

  • Object safetyは意外と難しい
  • トレイトがObject safeであっても、メンバ関数がselfを取る場合はその関数を呼び出せない(Self: ?Sizedなので)
  • Object safeなトレイトであっても、トレイトオブジェクトを作成するためにはAssociated types を指定し尽くす必要がある。

なお、Associated typesの指定は以下の場所で行える。

  • トレイトをトレイトオブジェクト型として表す部分(dyn Trait<X = ...>)
  • トレイトが別のトレイトを継承する場合、その指定部(trait Tr1: Tr2<X = ...> {})

なお、以下の場所では行えない。

  • トレイトのメンバ関数のWhere節(fn method() where Self: Tr<X = ...>)
  1. まあ単純にi8と書けばよいのだが

  2. FnOnce::call_once()の第一引数は例外?

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5