Имя техники для вывода аргументов типа параметра типа?

9

Настройка: Давайте предположим, что у нас есть вызываемый Iteratorтип с параметром типа Element:

interface Iterator<Element> {}

Тогда у нас есть интерфейс, Iterableкоторый имеет один метод, который будет возвращать Iterator.

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

Проблема с Iteratorуниверсальностью заключается в том, что мы должны предоставить ему аргументы типа.

Одна из идей для решения этой проблемы состоит в том, чтобы «вывести» тип итератора. Следующий псевдокод выражает идею, что существует переменная типа, Elementкоторая выводится как аргумент типа Iterator:

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

И тогда мы используем это где-то так:

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

Это определение IterableБезразлично»т использовать Elementгде - либо еще в его определении , но мой реальный вариант использования делает. Определенные функции, которые используют, Iterableтакже должны иметь возможность ограничивать свои параметры, чтобы принимать Iterables, которые возвращают только определенные виды итераторов, такие как двунаправленный итератор, поэтому возвращаемый итератор параметризован, а не только тип элемента.


Вопросов:

  • Есть ли установленное имя для этих предполагаемых переменных типа? А как насчет техники в целом? Незнание конкретной номенклатуры затруднило поиск таких примеров в дикой природе или изучение специфических особенностей языка.
  • Не все языки с генериками имеют эту технику; есть ли названия для подобных методов в этих языках?
Леви Моррисон
источник
1
Можете ли вы показать код, который не компилируется, который вы хотите скомпилировать? Я думаю, что это прояснит вопрос.
Подметальная
1
Вы также можете выбрать один язык (или сказать, какой язык вы используете, и что означает синтаксис). Это явно не, например, C #. В Интернете доступно много информации о «типовых выводах», но я не уверен, что она применима здесь.
5
Я реализую дженерики на языке, а не пытаюсь получить код на одном языке для компиляции. Это также вопрос именования и дизайна. Вот почему это несколько агностик. Не зная терминов, это затрудняет поиск примеров и документации на существующих языках. Конечно, это не единственная проблема?
Леви Моррисон

Ответы:

2

Я не знаю, есть ли конкретный термин для этой проблемы, но есть три основных класса решений:

  • избегать конкретных типов в пользу динамической отправки
  • разрешить параметры типа заполнителя в ограничениях типа
  • избегайте параметров типа, используя связанные типы / семейства типов

И, конечно, стандартное решение: продолжайте указывать все эти параметры.

Избегайте конкретных типов.

Вы определили 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. Это означает, что выражение типа 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, в которой обсуждаются связанные типы. Но учтите, что некоторые моменты в пользу связанных типов вместо общих применяются только там, потому что язык не имеет подтипов, и каждая черта может быть реализована не более одного раза для каждого типа. Т.е. черты Rust не являются Java-подобными интерфейсами.

Другие интересные системы типов включают в себя Haskell с различными языковыми расширениями. Модули / функторы OCaml являются сравнительно простой версией семейств типов, без непосредственного смешения их с объектами или параметризованными типами. Java отличается ограничениями в своей системе типов, например, обобщениями с удалением типов и отсутствием обобщений над типами значений. C # очень похож на Java, но ему удается избежать большинства этих ограничений за счет увеличения сложности реализации. Scala пытается интегрировать дженерики в стиле C # с классами типов в стиле Haskell поверх платформы Java. Обманчиво простые шаблоны C ++ хорошо изучены, но не похожи на большинство реализаций дженериков.

Также стоит взглянуть на стандартные библиотеки этих языков (особенно на стандартные коллекции библиотек, такие как списки или хеш-таблицы), чтобы увидеть, какие шаблоны обычно используются. Например, C ++ имеет сложную систему различных возможностей итератора, а Scala кодирует возможности детального сбора в качестве признаков. Стандартные интерфейсы библиотеки Java иногда не работают, например Iterator#remove(), но могут использовать вложенные классы как своего рода связанный тип (например Map.Entry).

Амон
источник