web-dev-qa-db-ja.com

型パラメーターの型引数を推論する手法の名前は?

Setup:タイプパラメータIteratorを持つElementと呼ばれるタイプがあるとします。

interface Iterator<Element> {}

次に、Iterableを返す1つのメソッドを持つインターフェースIteratorがあります。

// T has an upper bound of Iterator
interface Iterable<T: Iterator> {
    getIterator(): T
}

一般的なIteratorの問題は、型引数を指定する必要があることです。

これを解決する1つのアイデアは、イテレータのタイプを「推測」することです。次の疑似コードは、Elementの型引数であると推定される型変数Iteratorがあるという考えを表しています。

interface <Element> Iterable<T: Iterator<Element>> {
    getIterator(): T
}

そして、次のように使用します。

class Vec<Element> implements Iterable<VecIterator<Element>> {/*...*/}

このIterableの定義は、その定義の他の場所ではElementを使用していませんが、私の実際のユースケースでは使用しています。 Iterableを使用する特定の関数は、双方向の反復子など、特定の種類の反復子のみを返すIterablesを受け入れるようにパラメーターを制約できる必要もあります。そのため、返される反復子は代わりにパラメーター化されます。要素タイプのみの。


質問:

  • これらの推定された型変数の確立された名前はありますか?テクニック全体についてはどうですか?特定の命名法がわからないため、この例を実際に検索したり、言語固有の機能について学ぶことが困難になっています。
  • 総称を持つすべての言語にこの手法があるわけではありません。これらの言語でsimilarテクニックの名前はありますか?
9
Levi Morrison

この問題に特定の用語があるかどうかはわかりませんが、3つの一般的な解決策があります。

  • 動的ディスパッチを支持して具体的な型を避ける
  • 型制約でプレースホルダー型パラメーターを許可する
  • 関連するタイプ/タイプファミリーを使用してタイプパラメータを回避する

そしてもちろん、デフォルトの解決策:これらのパラメーターをすべてスペルアウトし続けます。

具体的な型は避けてください。

Iterableインターフェースを次のように定義しました:

_interface <Element> Iterable<T: Iterator<Element>> {
    getIterator(): T
}
_

これにより、ユーザーはイテレータの正確な具象型Tを取得できるため、インターフェースの最大のパワーが得られます。これにより、コンパイラーはインライン化などの最適化をさらに適用できます。

ただし、_Iterator<E>_が動的にディスパッチされるインターフェイスである場合、具象型を知る必要はありません。これは、例えばJavaが使用するソリューション。インターフェイスは次のように記述されます。

_interface Iterable<Element> {
    getIterator(): Iterator<Element>
}
_

これの興味深いバリエーションは、Rustの_impl Trait_構文です。これにより、抽象的な戻り値の型を使用して関数を宣言できますが、具体的な型は呼び出し側で認識されます(したがって、最適化が可能です)。これは、暗黙の型パラメーターと同様に動作します。

プレースホルダータイプのパラメーターを許可します。

Iterableインターフェースは、エレメントタイプについて知る必要がないため、次のように書くことができる場合があります。

_interface Iterable<T: Iterator<_>> {
    getIterator(): T
}
_

ここで、_T: Iterator<_>_は、「Tは要素タイプに関係なく任意の反復子です」という制約を表します。より厳密には、「Elementが_Iterator<Element>_になるように、Tの型が存在します」と表現できます。Elementの具体的な型を知る必要はありません。つまり、型式_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ブックの Advanced Traits の章を読むことを検討してください。ただし、言語にはサブタイピング機能がなく、各特性はタイプごとに最大1回しか実装できないため、ジェネリックではなく関連タイプを支持するいくつかの点が適用されることに注意してください。つまりRustトレイトはJavaのようなインターフェースではありません。

他の興味深い型システムには、さまざまな言語拡張を備えたHaskellが含まれます。 OCaml modules/functors は、オブジェクトやパラメーター化された型と直接混在しないタイプファミリーの比較的単純なバージョンです。 Javaは、その型システムの制限で注目に値します。型消去を伴うジェネリックで、値型に対するジェネリックはありません。 C#は非常にJavaに似ていますが、実装の複雑さが増す代わりに、これらの制限のほとんどを回避できます。 Scalaは、C#スタイルのジェネリックとJavaプラットフォーム上でHaskellスタイルの型クラスを統合しようとします。 C++の一見単純なテンプレートはよく研究されていますが、ほとんどのジェネリック実装とは異なります。

これらの言語の標準ライブラリ(特にリストやハッシュテーブルなどの標準ライブラリコレクション)を見て、どのパターンが一般的に使用されているかを確認することも価値があります。例えば。 C++には、さまざまなイテレーター機能の複雑なシステムがあり、Scalaは、きめの細かい収集機能を特性としてエンコードします。 Javaの標準ライブラリインターフェースは、音が悪い場合があります。 Iterator#remove()、ただし、ネストされたクラスを一種の関連タイプとして使用できます(例:_Map.Entry_)。

2
amon