以下の駄文は独自研究を多く含んでおり、間違っている可能性は大いにある。
実際の所、後日処理系のコードを読んで確認するつもりである。(そのためのメモという意味合いもある)
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>
を同時に実装することができる(例えば型u32
はFrom<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
型に型強制しているのである。i8
はDebug
トレイトを実装し、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 typesTr::X
を持つ - 型
T
が traitTr
を実装している場合、 Associated typesTr::X
はT
に依存して一意に決まる - 上記の例で
S1
,S2
,dyn Tr
はそれぞれ型であり、traitTr
を実装している - 実際、
<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>::X
はTr1
の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 = ...>
)