Статьи

Почему я не доверяю групповым символам и зачем они нам нужны?

В любом языке программирования, который сочетает полиморфизм подтипа (ориентация объекта) с параметрическим полиморфизмом (обобщение), возникает вопрос о дисперсии . Предположим, у меня есть список строк, типа List<String> . Могу ли я передать это функции, которая принимает List<Object> ? Давайте начнем с этого определения:

1
2
3
4
5
interface List<T> {
    void add(T element);
    Iterator<T> iterator();
    ...
}

Сломанная ковариация

Интуитивно, мы можем сначала подумать, что это должно быть разрешено. Это выглядит хорошо:

1
2
3
4
5
void iterate(List<Object> list) {
    Iterator<Object> it = list.iterator();
    ...
}
iterate(ArrayList<String>());

Действительно, некоторые языки, включая Eiffel и Dart, принимают этот код. К сожалению, это несостоятельно, как можно видеть в следующем примере:

1
2
3
4
5
6
//Eiffel/Dart-like language with
//broken covariance:
void put(List<Object> list) {
    list.add(10);
}
put(ArrayList<String>());

Здесь мы передаем List<String> функции, принимающей List<Object> , которая пытается добавить Integer в список.

Java делает ту же ошибку с массивами. Следующий код компилируется:

1
2
3
4
5
//Java:
void put(Object[] list) {
    list[0]=10;
}
put(new String[1]);

Сбой во время выполнения с ArrayStoreException .

Разница в использовании сайта

Однако в Java используется другой подход для универсальных классов и типов интерфейсов. По умолчанию тип класса или интерфейса является инвариантным , то есть:

  • присваивается L<V> тогда и только тогда, когда U точно такого же типа, как и V

Поскольку в большинстве случаев это крайне неудобно, Java поддерживает так называемую вариацию use-site , где:

  • L<U> присваивается L<? extends V> L<? extends V> если U является подтипом V , и
  • L<U> присваивается L<? super V> L<? super V> если U является супертипом V

Уродливый синтаксис ? extends V ? extends V или ? super V ? super V называется подстановочным знаком . Мы также говорим, что:

  • L<? extends V> L<? extends V> является ковариантным в V , и что
  • L<? super V> L<? super V> контравариантно в V

Поскольку подстановочная нотация в Java так безобразна, мы больше не будем ее использовать в этом обсуждении. Вместо этого мы напишем символы подстановки, используя ключевые слова для контравариантности и ковариации соответственно. Таким образом:

  • L<out V> является ковариантным в V , и
  • L<in V> является контравариантным в V

Данный V называется границей подстановочного знака:

  • out Vограниченный сверху подстановочный знак, а V — его верхняя граница, и
  • in V — ограниченный снизу подстановочный знак, а V — его нижняя граница.

Теоретически, мы могли бы иметь подстановочный знак с верхней и нижней границами, например, L<out X in Y> .
Мы можем выразить несколько верхних границ или несколько нижних границ, используя тип пересечения, например, L<out U&V> или L<in U&V> .
Обратите внимание, что выражения типа L<out Anything> и L<in Nothing> относятся к одному и тому же типу, и этот тип является супертипом всех экземпляров L
Вы часто будете видеть, что люди ссылаются на подстановочные типы как на экзистенциальные типы . Под этим они понимают, что если я знаю, что list имеет тип List<out Object> :

1
List<out Object> list;

Тогда я знаю, что существует неизвестный тип T , подтип Object , такой, что list имеет тип List<T> .
В качестве альтернативы мы можем взять более цейлонскую точку зрения и сказать, что List<out Object> является объединением всех типов List<T> где T является подтипом Object .
В системе с дисперсией использования сайта следующий код не компилируется:

1
2
3
4
5
void iterate(List<Object> list) {
    Iterator<Object> it = list.iterator();
    ...
}
iterate(ArrayList<String>()); //error: List<String> not a List<Object>

Но этот код делает:

1
2
3
4
5
void iterate(List<out Object> list) {
    Iterator<out Object> it = list.iterator();
    ...
}
iterate(ArrayList<String>());

Правильно, этот код не компилируется:

1
2
3
4
void put(List<out Object> list) {
    list.add(10); //error: Integer is not a Nothing
}
put(ArrayList<String>());

Теперь мы у входа в кроличью нору. Чтобы интегрировать типы с подстановочными символами в систему типов, отвергая при этом необоснованный код, как в приведенном выше примере, нам нужен гораздо более сложный алгоритм для подстановки аргументов типа.

Участник печатает в вариации использования сайта

То есть, когда у нас есть универсальный тип, такой как List<T> , с методом void add(T element) , вместо простой замены Object на T , как мы делаем с обычными инвариантными типами, нам нужно рассмотреть дисперсию местоположение, в котором встречается параметр типа. В этом случае T встречается в противоположном расположении типа List , а именно как тип параметра метода. Сложный алгоритм, который я не буду здесь записывать, говорит нам, что мы должны заменить Nothing , нижний тип, в этом месте.
Теперь представьте, что в нашем интерфейсе List есть метод partition() с такой подписью:

1
2
3
4
interface List<T> {
    List<List<T>> partition(Integer length);
    ...
}

Каков возвращаемый тип partition() для List<out Y> ? Ну, не теряя точности, это:

1
List<in List<in Y out Nothing> out List<in Nothing out Y>>

Уч.
Поскольку никто в здравом уме не хочет думать о таких типах, разумный язык отбрасывает некоторые из этих границ, оставляя что-то вроде этого:

1
List<out List<out Y>>

Что смутно приемлемо. К сожалению, даже в этом очень простом случае мы уже далеко за пределами того, что программист может легко следить за тем, что делает проверщик типов.
Итак, вот суть того, почему я не доверяю дисперсии использования сайта:

  • Сильный принцип в дизайне Цейлона заключается в том, что программист должен всегда иметь возможность воспроизвести рассуждения компилятора. Очень сложно рассуждать о некоторых сложных типах, возникающих из-за дисперсии сайта использования.
  • Это имеет вирусный эффект: как только эти подстановочные знаки закрепляются в коде, они начинают распространяться, и довольно сложно вернуться к моим обычным инвариантным типам.

Декларация сайта отклонений

Гораздо более разумная альтернатива использованию-сайт-дисперсия — это объявление-сайт-дисперсия , где мы определяем дисперсию универсального типа при объявлении. Это система, которую мы используем на Цейлоне. В этой системе нам нужно разделить List на три интерфейса:

01
02
03
04
05
06
07
08
09
10
11
12
interface List<out T> {
     Iterator<T> iterator();
     List<List<T>> partition(Integer length);
     ...
}
  
interface ListMutator<in T> {
    void add(T element);
}
  
interface MutableList<T>
    satisfies List<T>&ListMutator<T> {}

List объявлен ковариантным типом, ListMutator — контравариантным типом, а MutableList — инвариантным подтипом обоих типов.
Может показаться, что требование наличия нескольких интерфейсов является большим недостатком различий между сайтами объявлений, но часто оказывается полезным отделить мутацию от операций чтения, и:

  • мутационные операции очень часто инвариантны, тогда как
  • операции чтения очень часто являются ковариантными.

Теперь мы можем написать наши функции так:

01
02
03
04
05
06
07
08
09
10
void iterate(List<Object> list) {
    Iterator<Object> it = list.iterator();
    ...
}
iterate(ArrayList<String>());
  
void put(ListMutator<Integer> list) {
    list.add(10);
}
put(ArrayList<String>()); //error: List<String> is not a ListMutator<Integer>

Вы можете прочитать больше о декларации сайта дисперсии здесь .

Почему нам нужна вариация использования сайта на Цейлоне

К сожалению, в Java нет различий между сайтами объявлений, и чистое взаимодействие с Java очень важно для нас. Мне не нравится добавлять основные функции в систему типов нашего языка исключительно для целей взаимодействия с Java, и поэтому я несколько лет сопротивлялся добавлению подстановочных знаков в Ceylon. В итоге реальность и практичность победили, а мое упрямство проиграло. Итак, в Ceylon 1.1 теперь есть разница между сайтами использования с одинарными подстановочными знаками.
Я пытался сохранить эту функцию как можно более жестко ограниченной, с минимальным необходимым для приличного взаимодействия Java. Это означает, что, как в Java:

  • нет двойных подстановочных знаков формы List<in X out Y> и
  • тип с подстановочными символами не может встречаться в extends или satisfies определению класса или интерфейса.

Кроме того, в отличие от Java:

  • не существует неявно ограниченных подстановочных знаков, верхние границы всегда должны быть записаны явно, и
  • поддержка подстановочных знаков не поддерживается.

Захват подстановочного знака — очень умная особенность Java, которая использует «экзистенциальную» интерпретацию подстановочного типа. Дана универсальная функция, подобная этой:

1
List<T> unmodifiableList<T>(List<T> list) => ... :

Java позволил бы мне вызывать unmodifiableList() , передавая подстановочный тип, такой как List<out Object> , возвращая другой подстановочный знак List<out Object> , полагая, что существует какой-то неизвестный X , подтип Object для которого вызов будет иметь типизированный тип. , То есть этот код считается хорошо типизированным, хотя тип List<out Object> не может быть назначен List<T> для любого T :

1
2
List<out Object> objects = .... ;
List<out Object> unmodifiable = unmodifiableList(objects);

В Java ошибки ввода, связанные с захватом подстановочных знаков, почти невозможно понять, так как они включают неизвестный и не подлежащий определению тип. Я не планирую добавлять поддержку захвата с подстановочными знаками на Цейлоне.

Попробуйте это

Разница в использовании сайта уже реализована и уже работает в Ceylon 1.1, которую вы можете получить из GitHub, если у вас супер-мотивация.
Несмотря на то, что основной мотивацией для этой функции было отличное взаимодействие с Java, будут и другие, надеюсь, редкие случаи, когда подстановочные знаки будут полезны. Это, однако, не указывает на какой-либо значительный сдвиг в нашем подходе. Мы продолжим использовать дисперсию декларации на сайте в Ceylon SDK, за исключением крайних случаев.

ОБНОВИТЬ:
Я только что понял, что забыл сказать спасибо Россу Тэйту за помощь в тонкостях алгоритма набора членов для использования дисперсии сайта. Очень хитрый материал, который Росс знает по макушке!