Rustのself引数まとめ
概要: Rustの随所でself引数は特別扱いされている。それらの挙動について調べた。
self引数とメソッド
Rustではnon-staticメソッドは self
という特殊な名前の引数を持つ関数として定義されている。例えば、
struct A; // parse_self_arg impl A { fn f1(self: A) {} fn f2(self: &mut A) {} fn f3(self: &A) {} fn f4(self: Box<A>) {} // 生存期間を明示すると以下の通り // fn f2<'a>(self: &'a mut A) {} // fn f3<'a>(self: &'a A) {} }
と書くと、 f1
, f2
, f3
はメソッドになる。
self
はキーワードであり、この名前の引数は特定の条件下でのみ宣言できる。それは以下の場合である。
- traitまたはimpl内の関数の引数である。
- 第一引数である。
Self
,&Self
,&mut Self
,Box<Self>
のいずれかの型をもつ。(引数自体はmut
であってもなくてもよい)
selfショートカット構文
self
引数は頻出するため、以下の構文糖衣が用意されている。
struct A; // parse_self_arg impl A { fn f1(self) {} fn f1mut(mut self) {} fn f2(&mut self) {} fn f3(&self) {} // 生存期間を明示すると以下の通り // fn f2<'a>(&'a mut self) {} // fn f3<'a>(&'a self) {} // 以下と同じ // fn f1(self: Self) {} // fn f1mut(mut self: Self) {} // fn f2(self: &mut Self) {} // fn f3(self: &Self) {} // fn f2<'a>(self: &'a mut Self) {} // fn f3<'a>(self: &'a Self) {} }
生存期間の省略
関数宣言で生存期間の指定を省略した場合、一定の規則に基づいて生存期間が復元される。このときに self
変数が特別扱いされる。具体的には、以下の規則に基づいている。
- 入力側で生存期間が省略された場合、出現位置ごとに別々のfreshな生存期間が割り当てられる。
- 出力側で生存期間が省略された場合、以下の規則に基づき、全て同じ生存期間が割り当てられる。
- もし、参照型の
self
引数がある場合、その参照の生存期間が用いられる。 - もし、入力側に生存期間が1つだけ出現する場合、その生存期間が用いられる。
- それ以外の場合、コンパイルエラー。
- もし、参照型の
メソッド記法
レシーバーにドットをつける receiver.method(args)
という記法は、 self
引数を持つメソッドにのみ有効である。
object safety
trait objectを生成できるtraitには条件がある。これをobject safetyというのであった。
あるtraitがobject safeであるとは、
Self: Sized
制約がない、かつ- 束縛/where/スーパートレイトの制約におけるトレイトの型引数に
Self
が出現しない、かつ - 全てのメソッドがobject safeである。
ただし、あるメソッドがobject safeであるとは、そのメソッドに Self: Sized
制約がついているか、以下が満たされていることである。
self
引数を持ち、かつself
引数以外の引数・戻り値型にSelf
を含まず、かつ- メソッドが型引数をとらない。
ObsoleteVisiblePrivateTypesVisitor
後方互換性のために残されているprivateness checkerで、 self
引数が特別扱いされている。詳細は不明
self
引数を回避する利点
ほとんどの場合、上記の条件を満たす引数は self
にしてしまうほうが便利である。しかし std::rc::Rc
と std::sync::Arc
は self
を使わない。
impl<T: ?Sized> Rc<T> { pub fn downgrade(this: &Self) -> Weak<T> { ... } ... }
この場合、同じ型をもつ関数でも、 self
引数のもつ利点は受けられない。 Rc
や Arc
が self
を使わないのは、これが Deref
を実装するコンテナであり、 Rc<T>
のメソッド記法が T
のメソッド記法の名前空間を汚染しないようにしたいからである。
コンパイラの該当箇所
syntax::parse::parser::Parser::parse_self_arg
… self引数およびselfショートカットの構文解析syntax::ast::SelfKind
… self引数の抽象構文。値渡しのショートカット/参照渡しのショートカット/型を明示する記法syntax::ast::FnDecl::is_self
… ASTにおいて、関数がself
引数を持つかrustc::hir::AssociatedItemKind::Method
…is_self
を保持している。rustc::hir::lowering::LoweringContext::lower_trait_item_ref
,rustc::hir::lowering::LoweringContext::lower_impl_item_ref
… traitとimplそれぞれについて、ASTのhas_self
をHIRのhas_self
に保存している。rustc::ty::AssociatedItem
…method_has_self_argument
を保持している。rustc_typeck::check::compare_method
… trait implの関数がtraitの関数と一致しているか調べるときに、method_has_self_argument
の一致もチェックしている。rustc_typeck::check::wfcheck::CheckTypeWellFormedVisitor::check_method_receiver
…self
引数があるとき、その型はSelf
,&mut Self
,&Self
,Box<Self>
のいずれかであることをチェックしている。rustc_typeck::check::method::probe::ProbeContext::has_applicable_self
… あるアイテムがメソッド記法の探索対象かどうかを調べている。rustc::ty::TyCtxt::virtual_call_violation_for_method
…Sized
でないメソッドのobject safetyをチェックしている。
まとめ
Rustではメソッドの第一引数に self
という特別な名前をつけることができる。これによりメソッドに has_self
フラグが立ち、構文のみならず型システムにも影響を与える。
Rustトレイトの既定実装と否定実装
概要: SendやSyncなど一部のトレイトで採用されている機能である、既定実装と否定実装の挙動を調べた。
既定実装と否定実装について
既定実装(デフォルト実装, 自動実装, オート実装, default impl, auto impl)と否定実装(negative impl)はRust RFC 0019: Opt-in Builtin Traits (OIBITs)にて、unsafeトレイトとともに規定されている機能である。
OIBITsは、その名前に反して、「言語に組み込みとは限らないマーカートレイトについて、オプトアウト方式での実装を可能にする」という、非常に天邪鬼な機能である。
既定実装と否定実装は組み合わせて使われる。実際の標準ライブラリの例を見るとわかりやすい。
#![feature(optin_builtin_traits)] // unsafeトレイト unsafe trait Send {} // unsafeな既定実装 unsafe impl Send for .. {} // 否定実装 impl<T:?Sized> !Send for Arc<T> {}
標準ライブラリのOIBITs
1.16.0現在、標準ライブラリで既定実装と否定実装が使われているのは以下の4つである。
std::marker::Send
(core::marker::Send
)std::marker::Sync
(core::marker::Sync
)std::panic::UnwindSafe
std::panic::RefUnwindSafe
既定実装
既定実装は以下のような構文を持つ。
unsafe impl Trait for .. {} impl Trait for .. {}
ただし、以下の制約がある。
- この実装は生存期間・型パラメーターをとることができない。
- 実装対象のトレイトは生存期間・型パラメーターをとることができない。
- 既定実装は中身を持つことができない。したがってマーカートレイトに対してしか使うことができない。 (このトレイトがwhere節をもつことは可能である。)
既定実装の適用規則
既定実装をもつトレイトは、トレイト束縛の解決時にその内容が参照される。以下の条件で、既定実装が適用される。 (rustc::traits::select
2104行目)
- 否定実装を含め、他の実装が見つかっていない。 (
rustc::traits::select
1122行目)- where制約を満たしていない実装でも、型のパターンが一致していれば、この時点では「見つかっている」と見なす。
- トレイト自身にwhere制約がある場合、それを満たしている。
- この型を構成する型も、同じトレイトを実装している。ただし、「構成する型」は以下のように定義される。
これらが where
に追加されたのと同様に振る舞うと考えればよい。
既定実装の上書き
上に書いてあるように、既定実装の生成するwhere制約が所望のものではなかったときは、自分で定義した実装で上書きできる。
例えば、
#![feature(optin_builtin_traits)] trait Foo {} impl Foo for .. {} struct B<X>(X);
の場合、 B<X>
には以下の実装が生成されたような扱いになる。
impl<X: Foo> Foo for B<X> {}
例えば、以下のように書くと、 B
に対しては既定実装は適用されなくなり、かわりに明記された実装が使われるようになる。 X
はFooでなくてもよいが、Barである必要があるようになる。
trait Bar {} impl<X: Bar> Foo for B<X> {}
以下の場合、 B<u32>
については上記の実装を適用し、それ以外については B<X>
であっても既定実装を適用することになる。
impl Foo for B<u32> {}
否定実装
否定実装は以下のような構文を持つ。
impl<'a, X> !Trait for Type<'a, X> {}
ただし、以下の制約がある。
- 当たり前だが、inherent implとして (
impl !Type {}
のように) 使うことはできない。 - 実装対象のトレイトは既定実装を持つ必要がある。したがって否定実装は中身を持たないし、マーカートレイトに対してしか使うことができない。 (このトレイトがwhere節をもつことは可能である。)
否定実装は、この実装が採用されたときにエラーとなるという点以外は、普通の実装と同様である。これは既定実装の上書きをするときに、where条件を変えるのではなく実装を丸ごと禁止するのに使う。
まとめ
既定実装と否定実装は Send
/Sync
/UnwindSafe
/RefUnwindSafe
のように、基本的には特定の継承ルールに基づいてマーカートレイトを実装させたいが、特定の型に対しては異なるルールを適用したいときに用いる。
Rustの組み込みマクロ
Rustのマクロの多くは macro_rules!
で定義されるが、トークン列をトークン列に変換するものなら何でもマクロとして実装されうる。
Rustの標準ライブラリのマクロの多くは core::macros
にて定義されている。
以下のマクロは通常の macro_rules!
により定義される。
panic!
assert!
assert_eq!
assert_ne!
debug_assert!
debug_assert_eq!
debug_assert_ne!
try!
write!
writeln!
unreachable!
unimplemented!
以下のマクロは core::macros::builtin
に宣言だけ存在するが、実装はコンパイラ組み込みである。
format_args!
env!
option_env!
concat_idents!
concat!
line!
column!
file!
stringify!
include_str!
include_bytes!
module_path!
cfg!
include!
これらはコンパイラ組み込みであり、 syntax_ext::register_builtins
で登録されている。
これはマクロに限らない一般の構文拡張を管理するデータベース syntax::ext::base
で管理されている。このデータベースには以下のようなエントリがある(抜粋):
MultiModifier
: Pythonの属性のように、itemを受け取り別のitemに変換する構文拡張。ProcMacro
: トークン列をトークン列に変換する、構文拡張の一般的なインターフェース。AttrProcMacro
:ProcMacro
に似ているが、属性込みで変換できるインターフェース。NormalTT
:ProcMacro
に似ているが、よりマクロ呼び出しに特化したインターフェースになっている。トークン列ではなく、構文要素を返す。IdentTT
: 識別子を識別子に変換する。
マクロ構文では ProcMacro
, NormalTT
, IdentTT
のみ取り扱われる。他の構文拡張はattrやderiveで使われる。
macro_rules!
で定義されるマクロは syntax::ext::tt::macro_rules
で NormalTT
に変換される。また組み込みマクロも NormalTT
である。なお、 macro_rules!
自身はかなり特殊な扱いで、構文解析器が場合分けを行う。
組み込みマクロは、上で説明したように、トークン列を構文要素に変換する関数がコンパイラ内に実装されているという以上のことはない。 include_str!
や stringify!
のようなCプリプロセッサのような処理は syntax::ext::source_util
で実装されている。それ以外の組み込みマクロの実装は src/libsyntax_ext
内の各ソースファイルが提供している。
まとめ
Rustにおけるマクロは、トークン列を構文要素に変換するメタ関数に他ならない。多くは macro_rules!
により構文解析時に定義されるが、これで定義できるのはごく簡単なマクロだけである。より複雑だったり、処理系組み込みの機能を提供するマクロは、コンパイラに組み込みであったり、 別のcrateでプロシージャマクロとして定義されていたりする。
Rustのマングリング(名前修飾)
Rustの生成したネイティブコードを見ると、 _ZN6thread5sleep20h87eee61de4645181cAbE
のようなシンボル名が見える。この例は std::thread::sleep
に対応している。このようにRustの(単相化された)アイテム(関数や変数など)の名前をリンカが認識できる文字列にエンコードする処理はマングリングと呼ばれている。
Rustのマングリングは一見してわかるようにC++互換になっている。実際に上の関数をデマングルすると
$ c++filt _ZN6thread5sleep20h87eee61de4645181cAbE thread::sleep::h87eee61de4645181cAb
と、それらしき名前が出てくる。
しかし、これは表層上C++互換になっているというだけで、Rustのマングリングの本質はこのハッシュ部分 h87eee61de4645181cAb
に依拠している。
以下、Rust 1.15.1の rustc_trans::back::symbol_names
に基づいて、Rustのマングリングについて説明する。これはコンパイラのバージョンアップにより情報が古くなる可能性がある。
DefPathを計算する
まず、Rustのアイテムの単相化前の部分を一定の形にエンコードする必要があるが、これには rustc::hir::map::definitions::DefPath
が使われる。
DefPathは簡単に言うと、 std::result::Result::map
のように、単相化前のアイテムの位置を、crateのルートからの相対パスとして保持したものである。ただしそれだけでは関数内関数など、同じ名前で複数のアイテムを定義できる場合がある。そのため、 foo[1]::bar[0]
, foo[1]::bar[1]
といった感じで番号 (disambiguator) を与えて区別する。disambiguatorは0から順に振られるため、通常は 0
である。
DefPathの定義を抜粋すると以下のようになる。
pub struct DefPath { pub data: Vec<DisambiguatedDefPathData>, pub krate: CrateNum, } pub struct DisambiguatedDefPathData { pub data: DefPathData, pub disambiguator: u32 } pub enum DefPathData { CrateRoot, InlinedRoot(Box<InlinedRootPath>), Misc, Impl, TypeNs(InternedString), ValueNs(InternedString), Module(InternedString), MacroDef(InternedString), ClosureExpr, TypeParam(InternedString), LifetimeDef(InternedString), EnumVariant(InternedString), Field(InternedString), StructCtor, Initializer, Binding(InternedString), ImplTrait }
DefPath
も DisambiguatedDefPathData
も Hash
をderiveしているが、今回のハッシュ化に DefPath:Hash
の実装は使わない。というのも krate: CrateNum
は内部的なu32のidで表現されており、これをそのまま使うとコンパイラの実行ごとにハッシュが安定しないからである。
安定ハッシュを用意する
Rustにはオブジェクトをハッシュ化するための Hash
というトレイトが用意されているが、通常のハッシュ実装ではアーキテクチャ依存性が発生するためにクロスコンパイルで問題が生じる可能性がある。というのも、 Hash
が利用する Hasher
は以下のようなインターフェースを持ち、……
pub trait Hasher { fn finish(&self) -> u64; fn write(&mut self, bytes: &[u8]); fn write_u8(&mut self, i: u8) { ... } fn write_u16(&mut self, i: u16) { ... } fn write_u32(&mut self, i: u32) { ... } fn write_u64(&mut self, i: u64) { ... } fn write_usize(&mut self, i: usize) { ... } fn write_i8(&mut self, i: i8) { ... } fn write_i16(&mut self, i: i16) { ... } fn write_i32(&mut self, i: i32) { ... } fn write_i64(&mut self, i: i64) { ... } fn write_isize(&mut self, i: isize) { ... } }
……しかもこれらのデフォルト実装は、「現在のアーキテクチャのエンディアンでバイト列に変換して write
に突っ込む」となっているからである。
Rustのコンパイラはこれに対処するため、 isize
や usize
を使っても常に同じようにハッシュされるHasherである rustc_data_structures::stable_hasher::StableHasher
を用意している。これはblake2bのラッパーだが以下のように動作する:
i8
とu8
は1バイトとして書き込む。- 残りの全ての整数型は、リトルエンディアンの128bit整数に変換して書き込む。
型パラメーターとあわせてハッシュ化する
64bitの安定ハッシャーを用意して、以下の順に突っ込む。
- DefPath (ただしcrateが安定するように工夫してある)
- アイテム自身の型 (単相化する前)
- 多相ならば、その型代入
ハッシュを64bitで取り出し、16進表記して接頭辞 h
をつける。 (例: h87eee61de4645181cAb
)
飾り付けをする
再び、アイテムの絶対パスを計算して、先ほど生成したハッシュと連結する。 (例: thread
/sleep
/h87eee61de4645181cAb
)
サニタイズする
変な文字が入っているかもしれないので、簡単な文字だけからなるように変換する。
@
→$SP$
*
→$BP$
&
→$RF$
<
→$LT$
>
→$GT$
(
→$LP$
)
→$RP$
,
→$C$
:
→.
-
→.
a
からz
,A
からZ
,0
から9
,_
,.
,$
→ そのまま- それ以外のUnicode文字 →
$u2764$
のようにコードポイントを16進数で
C++互換でマングリングする
_ZN
+ (文字数を10進数で + 文字列) ×任意個 + E
できあがり
以上の処理は rustc_trans::trans_item::TransItem::compute_symbol_name
で使われている。
まとめ
Rustはアイテムの位置と型引数をできるだけ安定性の高い方法でハッシュ化し、それをシンボル名として使っている。読みやすさのために、C++互換の情報も付与されている。
ハッシュは h
+ 64bit で17文字のはずなのに実例をみると20文字になっている原因がまだ把握できていない。
Rustのvtableの内部構造
trait objectは、型情報を忘れるかわりにvtableへの参照を持ち回すことで動的ディスパッチを実現している。
vtableを生成するコードはrustc_trans::meth
112行目にある。これによると、vtableの構造は以下のとおり。
- 0番目: Drop Glue をあらわす関数ポインタ (デストラクタ)
- 1番目: 元の型のバイト数
- 2番目: 元の型のアラインメント
- 3番目以降: メソッドへのポインタ (宣言順)
Self: Sized
制約のついたメソッドに対応するスロットには0が代入される。
trait objectはSizedではないので、値をコピー渡しすることはなく、fat pointerで渡す。fat pointerの0番目は、もとのデータと同じ先頭番地を指すポインタで、fat pointerの1番目が、vtableの先頭番地を指すポインタである。
実際にvtableを手動で取り出して実験してみたものが以下のコードである。
use std::mem::transmute; trait Foo { fn fn1(&self, u32); fn fn2(&self, u32) where Self: Sized; fn fn3(&self, u32); } #[derive(Debug)] struct S1(u64, u64, u64); impl Foo for S1 { fn fn1(&self, x: u32) { println!("fn1({:?}, {})", self, x); } fn fn2(&self, x: u32) where Self: Sized { println!("fn2({:?}, {})", self, x); } fn fn3(&self, x: u32) { println!("fn3({:?}, {})", self, x); } } impl std::ops::Drop for S1 { fn drop(&mut self) { println!("drop({:?})", self); } } struct FooVtable { drop_glue: fn(&mut S1), size: usize, align: usize, fn1_ptr: fn(&S1, u32), fn2_ptr: fn(&S1, u32), fn3_ptr: fn(&S1, u32), } fn recover_S1(foo: &mut Foo) -> (&mut S1, &'static FooVtable) { unsafe { transmute(foo) } } fn main() { let mut x = S1(3, 4, 5); let foo : &mut Foo = &mut x; let (xx, vtbl) = recover_S1(foo); println!("vtbl.drop_glue = {:x}", vtbl.drop_glue as usize); println!("vtbl.size = {:x}", vtbl.size); println!("vtbl.align = {:x}", vtbl.align); println!("vtbl.fn1_ptr = {:x}", vtbl.fn1_ptr as usize); println!("vtbl.fn2_ptr = {:x}", vtbl.fn2_ptr as usize); println!("vtbl.fn3_ptr = {:x}", vtbl.fn3_ptr as usize); println!("S1::fn1 = {:x}", S1::fn1 as usize); println!("S1::fn2 = {:x}", S1::fn2 as usize); println!("S1::fn3 = {:x}", S1::fn3 as usize); (vtbl.drop_glue)(xx); (vtbl.fn1_ptr)(xx, 88); (vtbl.fn3_ptr)(xx, 188); }
vtbl.drop_glue = 55c14c8def80 vtbl.size = 18 vtbl.align = 8 vtbl.fn1_ptr = 55c14c8df0c0 vtbl.fn2_ptr = 0 vtbl.fn3_ptr = 55c14c8df340 S1::fn1 = 55c14c8df0c0 S1::fn2 = 55c14c8df200 S1::fn3 = 55c14c8df340 drop(S1(3, 4, 5)) fn1(S1(3, 4, 5), 88) fn3(S1(3, 4, 5), 188) drop(S1(3, 4, 5))
drop glueは別として、メソッドテーブルに入っている関数ポインタは元のメソッドの使い回しであることがわかる。
むろん、このような荒技を実験以外に使うのは好ましくない。
RustでPhantomDataにT以外のものを入れるのはなぜか
PhantomDataにT以外のものを入れるのは、主に3つの理由がある。
- 生存期間変数に言及するため。
- 変性を制御するため。
- 所有関係を制御するため。
生存期間変数に言及するため
型だけではなく、生存期間変数も、迷子になっては困る。こういうときは参照を使って PhantomData<&'a T>
のように書くことが多い。
変性を制御するため
PhantomData<T>
は T
に対して共変である。つまり、 T
より広い型 S
があったとき(これは通常、生存期間変数を入れ替えることで発生する)、 PhantomData<S>
は PhantomData<T>
より広いとみなされる。
現在のRustは、内部的には非変・共変・反変・双変の4つの変性を持つ。反変が今後も残るかはわからないが、非変と共変のどちらをとるかは場合による。
非変にするときはよく PhantomData<fn(T) -> T>
と書く。
所有関係を制御するため
Drop Checkの関係で、「ある型がある型を(直接的に/間接的に)所有しているか」というのがborrow checkerの挙動に影響を与えてしまう。
この所有判定において、 PhantomData<T>
は T
を所有しているとみなされる。所有していないとみなしたい場合は PhantomData<*const T>
や PhantomData<&'a T>
を使うことが多い。
まとめ
PhantomDataは単に型や生存期間変数を迷子にしないという以上に、変性と所有関係という2つの隠し属性の制御にも用いられる。これらのためのイディオムがいくつか知られている。
Rustで型の多相再帰はできない
OCamlやHaskellに比べると、Rustは多相再帰ができない場合がほとんどである。以下にその詳細を説明する。
多相再帰
異なる型引数による再帰呼び出しを多相再帰 (polymorphic recursion) という。多相再帰はPurely Functinoal Data Structuresで紹介されているようなデータ構造でよく出てくる。例えば、完全二分木はOCamlとHaskellではそれぞれ以下のように書ける。
type 'a sep = Nil | Cons of ('a * 'a) sep
data Sep a = Nil | Cons (Sep (a, a))
これがlistの定義と異なることがわかるだろうか。listでは 'a list
の定義に 'a list
という形の型のみを用いる。ここでは 'a sep
の定義に ('a * 'a) sep
を用いている。これが多相再帰である。
Rustにおける型の多相再帰
同じものをRustで書こうとすると次のようになる。
enum Sep<T> { Nil, Cons(T, Box<Sep<(T, T)>>), } fn main() { let x = Sep::Nil::<u32>; }
しかしこれはコンパイルが通らない。例えばコンパイラが無限ループに陥りkillされる。
再帰型だけではなく再帰関数についても同じことが起こる。以下のコードはやはり再帰制限に引っかかる。
fn f<T>(n: u32, x: T) { if n > 0 { f::<Option<T>>(n-1, Some(x)); } } fn main() { f(1, true); }
多相再帰ができない理由
OCamlやHaskellはパラメーター多相性を実行時に解決している(ジェネリックス)のに対し、Rustはパラメーター多相性をコンパイル時に解決する(テンプレート)。
上の例では、 Sep<u32>
ができたことにより、Rustは Sep<u32>
のための専用のコードを生成しようとする。ところが、この型は内部に Sep<(u32, u32)>
を持ちうるため、これに関係するコードの生成が必要になる。さらにその内部では Sep<((u32, u32), (u32, u32))>
が必要になる。この繰り返しになってしまうためコンパイルができない。
OCamlやHaskellなら、 int sep
専用ではなく 'a sep
用の汎用のコードを生成して終わりなので、特に困ることはない。
幽霊型も使用不可能
OCamlやHaskellの内部表現を真似て、 T
を Box
にくるんでもコンパイルエラーはなくならない。
enum Sep<T> { Nil, Cons(Box<T>, Box<Sep<(T, T)>>), } fn main() { let x = Sep::Nil::<u32>; }
完全に型情報を捨てて Any
にしてしまえば、もちろんコンパイルは通る。
use std::any::Any; enum Sep { Nil, Cons(Box<Any>, Box<Sep>), } fn main() { let x = Sep::Nil::<u32>; }
これだと心許ないので、幽霊型をつけてみると、コンパイルは失敗する。
struct BoxAny<T> { val: Box<std::any::Any>, _phantom: std::marker::PhantomData<T>, } enum Sep<T> { Nil, Cons(BoxAny<T>, BoxAny<Sep<(T, T)>>), } fn main() { let x = Sep::Nil::<u32>; }
結局、幽霊型の場合も、幽霊型引数の違いごとに別々にコード生成をしなくてはならないため、無限コード生成を回避することはできない。
多相再帰自体が禁止されているわけではない
実際は、多相再帰自体は禁止されているわけではない。無限にたくさんの型が発生しない限りは、多相再帰があっても問題ない。
人工的な例だが、次のようなプログラムは動作する。
use std::marker::PhantomData; struct Z; struct S<X>(PhantomData<X>); trait Nat { type Pred : Nat; fn to_u32() -> u32; } impl Nat for Z { type Pred = Z; fn to_u32() -> u32 { 0 } } impl<X:Nat> Nat for S<X> { type Pred = X; fn to_u32() -> u32 { X::to_u32() + 1 } } fn f<X:Nat>() -> u32 { if(X::to_u32() == 0) { 1 } else { f::<X::Pred>() * 2 } } fn main() { println!("{}", f::<S<S<S<Z>>>>()); }
ここで f<S<S<S<Z>>>>
を生成しようとすると f<S<S<Z>>>
が必要になる。これにはさらに f<S<Z>>
が必要になる。さらに f<Z>
が必要になる。しかし Z::Pred
は Z
だから、これ以上必要なものはない。
生存期間に関する多相再帰は可能
ここまでで言及したのは、型に関する多相再帰である。生存期間は型とは異なり、コンパイル時には型消去されるだけなので、実行時に無限に多くの生存期間が生じうることに全く問題はない。
むしろ、普通にRustコードを書いていると、生存期間に関する多相再帰が発生していると考えてよいだろう。例えば、
enum List<X> { Nil, Cons(X, Box<List<X>>), } fn length<'a, X>(list: &'a List<X>) -> u32 { match list { &List::Nil => 0, &List::Cons(_, ref tail) => length(tail) + 1, } } fn main() {}
において、 list
と tail
の生存期間は明らかに異なる。