Я не знаю, чи існує конкретний термін для цієї проблеми, але є три загальні класи рішення:
- уникайте конкретних типів на користь динамічної відправки
- дозволити параметри типу заповнювача в обмеженнях типу
- уникайте параметрів типу за допомогою асоційованих типів / типів сімей
І звичайно рішення за замовчуванням: продовжуйте правопис усіх цих параметрів.
Уникайте типів бетону.
Ви визначили Iterable
інтерфейс як:
interface <Element> Iterable<T: Iterator<Element>> {
getIterator(): T
}
Це дає користувачам інтерфейсу максимальну потужність, оскільки вони отримують точний конкретний тип T
ітератора. Це також дозволяє компілятору застосовувати більше оптимізацій, таких як вбудована лінія.
Однак якщо Iterator<E>
це динамічно розсилається інтерфейс, то знати конкретний тип не потрібно. Це, наприклад, рішення, яке використовує Java. Потім інтерфейс буде записаний як:
interface Iterable<Element> {
getIterator(): Iterator<Element>
}
Цікавим варіантом цього є impl Trait
синтаксис Руста, який дозволяє оголосити функцію абстрактним типом повернення, але знаючи, що конкретний тип буде відомий на сайті виклику (таким чином, дозволяючи оптимізувати). Це поводиться аналогічно параметру неявного типу.
Дозволити параметри типу заповнювача.
Iterable
Інтерфейс не потрібно знати про тип елемента, так що можна було б написати це як:
interface Iterable<T: Iterator<_>> {
getIterator(): T
}
Там, де T: Iterator<_>
виражено обмеження, "T - це будь-який ітератор, незалежно від типу елемента". Більш жорстко ми можемо це висловити так: "існує такий тип, Element
що T
є Iterator<Element>
", не знаючи жодного конкретного типу 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> {
...
}
Однак це лише особливість ергономіки мови і не робить мову більш потужною.
Системи типу складні, тому добре поглянути на те, що працює, а що не працює на інших мовах.
Наприклад, прочитайте розділ « Розширені риси» у «Книзі іржі», де обговорюються пов’язані типи. Але зауважте, що деякі моменти на користь асоційованих типів замість генеричних застосовуються лише там, оскільки мова не містить підтипів, і кожна ознака може бути реалізована максимум один раз на тип. Тобто риси іржі не є інтерфейсами, подібними до Java.
Інші цікаві системи типу включають Haskell з різними мовними розширеннями. Модулі / функтори OCaml є порівняно простою версією типів сімейства типів, не переплітаючи їх безпосередньо з об'єктами або параметризованими типами. Java примітний обмеженнями в своїй системі типів, наприклад, дженерики зі стиранням типу та відсутність генеричних даних щодо типів значень. C # дуже схожий на Java, але вдається уникнути більшості цих обмежень за рахунок підвищеної складності впровадження. Scala намагається інтегрувати загальну мову C # -style із типовими класами Haskell на вершині платформи Java. Оманливо прості шаблони C ++ добре вивчені, але на відміну від більшості реалізованих дженериків.
Також варто переглянути стандартні бібліотеки цих мов (особливо стандартні колекції бібліотек, такі як списки або хеш-таблиці), щоб побачити, які шаблони зазвичай використовуються. Наприклад, C ++ має складну систему різних можливостей ітератора, і Scala кодує дрібнозернисті можливості збору як риси. Інтерфейси стандартної бібліотеки Java іноді не є звуковими, наприклад Iterator#remove()
, але можуть використовувати вкладені класи як певний тип асоційованого типу (наприклад Map.Entry
).