この問題に特定の用語があるかどうかはわかりませんが、解決策には3つの一般的なクラスがあります。
- 動的ディスパッチを支持して具体的な型を避ける
- 型制約でプレースホルダー型パラメーターを許可する
- 関連するタイプ/タイプファミリーを使用してタイプパラメータを回避する
そしてもちろん、デフォルトのソリューション:これらのパラメーターをすべてスペルアウトし続けます。
具体的なタイプは避けてください。
Iterable
インターフェースを次のように定義しました:
interface <Element> Iterable<T: Iterator<Element>> {
getIterator(): T
}
これによりT
、イテレータの具体的なタイプを正確に取得できるため、インターフェースのユーザーに最大のパワーを提供します。これにより、コンパイラーはインライン化などの最適化をさらに適用できます。
ただし、Iterator<E>
が動的にディスパッチされるインターフェースである場合、具象タイプを知る必要はありません。これは、たとえばJavaが使用するソリューションです。インターフェイスは次のように記述されます。
interface Iterable<Element> {
getIterator(): Iterator<Element>
}
これの興味深いバリエーションはimpl Trait
、抽象的な戻り値の型で関数を宣言できるRustの構文ですが、具体的な型は呼び出し側で認識されるため(最適化が可能です)。これは、暗黙の型パラメーターと同様に動作します。
プレースホルダータイプのパラメーターを許可します。
Iterable
としてこれを書き込むことが可能であるかもしれないので、インタフェースは、要素の型について知る必要はありません。
interface Iterable<T: Iterator<_>> {
getIterator(): T
}
ここでT: Iterator<_>
、「Tは要素タイプに関係なく任意の反復子です」という制約を表します。より厳密に、我々はこのように表現することができます:「いくつかのタイプが存在するElement
ようT
であるIterator<Element>
」、のための任意の具体的な種類を知らなくてもElement
。つまり、type-expression Iterator<_>
は実際の型を記述せず、型制約としてのみ使用できます。
タイプファミリー/関連タイプを使用します。
たとえば、C ++では、型に型メンバーがある場合があります。これは、標準ライブラリ全体で一般的に使用されていますstd::vector::value_type
。これはすべてのシナリオでタイプパラメータの問題を実際に解決するわけではありませんが、タイプが他のタイプを参照する場合があるため、単一のタイプパラメータで関連タイプのファミリ全体を表すことができます。
定義しましょう:
interface Iterator {
type ElementType
fn next(): ElementType
}
interface Iterable {
type IteratorType: Iterator
fn getIterator(): IteratorType
}
次に:
class Vec<Element> implement Iterable {
type IteratorType = VecIterator<Element>
fn getIterator(): IteratorType { ... }
}
class VecIterator<T> implements Iterator {
type ElementType = T
fn next(): ElementType { ... }
}
これは非常に柔軟に見えますが、型の制約を表現することがより困難になる可能性があることに注意してください。たとえば、書かれIterable
ているとおり、イテレータ要素の型は強制されないため、interface Iterator<T>
代わりに宣言したい場合があります。そして、あなたはかなり複雑なタイプの計算を扱っています。そのような型システムを誤って決定不可能にすることは非常に簡単です(あるいは、おそらくそれはすでにそうなのでしょうか?)。
関連付けられた型は、型パラメーターのデフォルトとして非常に便利な場合があります。たとえば、Iterable
インターフェイスが要素タイプに対して個別のタイプパラメータを必要とすると仮定します。これは、常にではありませんが、反復子要素タイプと同じであり、プレースホルダタイプパラメータがあるとすると、次のように言うことができます。
interface Iterable<T: Iterator<_>, Element = T::Element> {
...
}
ただし、これは単なる言語の人間工学機能であり、言語をより強力にするものではありません。
型システムは難しいので、他の言語で何が機能し、何が機能しないかを確認することをお勧めします。
たとえば、Rust Book のAdvanced Traitsの章を読んでみてください。関連する型について説明しています。ただし、言語はサブタイプを備えておらず、各トレイトはタイプごとに最大1回しか実装できないため、ジェネリックではなく関連タイプを支持するいくつかのポイントが適用されることに注意してください。つまり、Rustの特性はJavaのようなインターフェースではありません。
その他の興味深い型システムには、さまざまな言語拡張を備えたHaskellがあります。OCamlのモジュール/ファンクターは、オブジェクトやパラメーター化された型と直接混在しないタイプファミリーの比較的単純なバージョンです。Javaは、その型システムの制限、たとえば型消去を伴うジェネリック、値型を超えるジェネリックがないことで注目に値します。C#は非常にJavaに似ていますが、実装の複雑さが増す代わりに、これらの制限のほとんどを回避できます。Scalaは、C#スタイルのジェネリックスをJavaプラットフォームの上にあるHaskellスタイルの型クラスと統合しようとします。C ++の一見単純なテンプレートはよく研究されていますが、ほとんどのジェネリック実装とは異なります。
これらの言語の標準ライブラリ(特にリストやハッシュテーブルなどの標準ライブラリコレクション)を見て、どのパターンが一般的に使用されているかを確認することも価値があります。たとえば、C ++にはさまざまなイテレーター機能の複雑なシステムがあり、Scalaは細かい収集機能を特性としてエンコードします。たとえばIterator#remove()
、Java標準ライブラリインターフェースは適切ではない場合がありますが、ネストされたクラスを一種の関連型(たとえばMap.Entry
)として使用できます。