Хорошая универсальная система типов

29

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

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

Если мы возьмем следующее в качестве целей проектирования такой системы типов:

  • Всегда создает легко читаемые объявления типов
  • Легко учиться (не нужно разбираться с ковариацией, контравариантностью и т. Д.)
  • максимизирует количество ошибок во время компиляции

Есть ли язык, который понял это правильно? Если я гуглю, единственное, что я вижу, это жалобы на то, как система типов отстой в языке X. Эта сложность присуща универсальной типизации? Должны ли мы просто отказаться от попыток проверить безопасность типов на 100% во время компиляции?

Мой главный вопрос: какой язык «правильно понял» в отношении этих трех целей? Я понимаю, что это субъективно, но до сих пор я не могу найти ни одного языка, где не все его программисты сходятся во мнении, что система родовых типов - беспорядок.

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

Питер
источник
2
Что вы имеете в виду easy-to-read type declarations? Третий критерий также неоднозначен: например, я могу превратить индекс массива из исключений границ в ошибки времени компиляции, не позволяя индексировать массивы, если я не смогу вычислить индекс во время компиляции. Кроме того, второй критерий исключает подтипы. Это не обязательно плохо, но вы должны знать, что вы спрашиваете.
Доваль
9
@gnat, это определенно не разглагольствование против Java. Я программирую почти исключительно на Java. Моя точка зрения заключается в том, что в сообществе Java общепринятым является то, что Generics имеет недостатки (не полный отказ, а, вероятно, частичный), поэтому логично задать вопрос, как они должны были быть реализованы. Почему они не правы, а другие правильно их поняли? Или на самом деле невозможно получить дженерики абсолютно правильно?
Питер
1
Если бы все просто воровали из C #, было бы меньше жалоб. Особенно Java в состоянии наверстать упущенное при копировании. Вместо этого они выбирают худшие решения. Многие вопросы, которые еще обсуждают комитеты по разработке Java, уже решены и реализованы в C #. Кажется, они даже не выглядят.
USR
2
@emodendroket: я думаю, что две мои самые большие жалобы на дженерики C # заключаются в том, что нет никакого способа применить ограничение «супертипа» (например Foo<T> where SiameseCat:T), и что нет возможности иметь дженерик, который не может быть преобразован в Object. ИМХО, .NET выиграл бы от агрегатных типов, которые были бы похожи на структуры, но еще более скромны. Если бы KeyValuePair<TKey,TValue>был такой тип, то IEnumerable<KeyValuePair<SiameseCat,FordFocus>>можно было бы привести к нему IEnumerable<KeyValuePair<Animal,Vehicle>>, но только если тип не может быть помещен в коробку.
суперкат

Ответы:

24

В то время как дженерики были основным направлением в сообществе функционального программирования на протяжении десятилетий, добавление дженериков к объектно-ориентированным языкам программирования создает некоторые уникальные проблемы, в частности взаимодействие подтипов и дженериков.

Однако, даже если мы сосредоточимся на объектно-ориентированных языках программирования, и в частности на Java, можно было бы разработать гораздо лучшую систему обобщений:

  1. Универсальные типы должны быть допустимы везде, где есть другие типы. В частности, если Tэто параметр типа, следующие выражения должны компилироваться без предупреждений:

    object instanceof T; 
    T t = (T) object;
    T[] array = new T[1];
    

    Да, для этого требуется, чтобы дженерики были реализованы, как и любой другой тип языка

  2. Ковариантность и контравариантность универсального типа должны быть указаны (или выведены из) его объявления, а не каждый раз, когда универсальный тип используется, поэтому мы можем написать

    Future<Provider<Integer>> s;
    Future<Provider<Number>> o = s; 
    

    скорее, чем

    Future<? extends Provider<Integer>> s;
    Future<? extends Provider<? extends Number>> o = s;
    
  3. Так как универсальные типы могут быть довольно длинными, нам не нужно указывать их избыточно. То есть мы должны уметь писать

    Map<String, Map<String, List<LanguageDesigner>>> map;
    for (var e : map.values()) {
        for (var list : e.values()) {
            for (var person : list) {
                greet(person);
            }
        }
    }
    

    скорее, чем

    Map<String, Map<String, List<LanguageDesigner>>> map;
    for (Map<String, List<LanguageDesigner>> e : map.values()) {
        for (List<LanguageDesigner> list : e.values()) {
            for (LanguageDesigner person : list) {
                greet(person);
            }
        }
    }
    
  4. Любой тип должен быть допустим в качестве параметра типа, а не только ссылочных типов. (Если мы можем иметь int[], почему мы не можем иметь List<int>)?

Все это возможно в C #.

меритон - забастовка
источник
1
Будет ли это также избавиться от дженериков, ссылающихся на себя? Что если я хочу сказать, что сопоставимый объект может сравнивать себя с чем-либо того же типа или подкласса? Это может быть сделано? Или, если я напишу метод сортировки, который принимает списки с сопоставимыми объектами, то все они должны быть сопоставимы друг с другом. Enum - еще один хороший пример: Enum <E расширяет Enum <E >>. Я не говорю, что система типов должна это делать, мне просто интересно, как C # справляется с этими ситуациями.
Питер
1
Вывод универсального типа в Java 7 и автоматическая помощь C ++ в некоторых из этих проблем, но являются синтаксическим сахаром и не изменяют базовые механизмы.
@Snowman Вывод типа Java имеет несколько действительно неприятных угловых случаев, например, вообще не работает с анонимными классами и не находит правильные границы для подстановочных знаков, когда вы оцениваете универсальный метод как аргумент другого универсального метода.
Доваль
@Doval, поэтому я сказал, что это помогает с некоторыми проблемами: это ничего не исправляет, и не решает все. Обобщения Java имеют много проблем: хотя они лучше, чем необработанные типы, они, безусловно, вызывают много головной боли.
34

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

Сравните это с дженериками Хаскелла, например. Они достаточно просты, поэтому, если вы используете вывод типа, вы можете написать правильную обобщенную функцию случайно . В самом деле, если указать один тип, компилятор часто говорит себе: «Ну, я был собираюсь сделать это родовым, но вы попросили меня сделать это только для Интс, так что угодно.»

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

Карл Билефельдт
источник
1
Спасибо за этот ответ. Эта статья начинается с некоторых примеров Джошуа Блоха, где дженерики становятся слишком сложными: artima.com/weblogs/viewpost.jsp?thread=222021 . Является ли это различием в культуре между Java и Haskell, где такие конструкции считаются хорошими в Haskell, или есть реальное отличие от системы типов Haskell, которая избегает таких ситуаций?
Питер
10
У @Peter Haskell нет подтипов, и, как сказал Карл, компилятор может автоматически выводить типы, включая ограничения типа «тип aдолжен быть целым числом».
Доваль
Другими словами, ковариация в таких языках, как Scala.
Пол Дрэйпер
14

Примерно 20 лет назад было проведено немало исследований по объединению генериков с подтипами. Язык программирования Thor, разработанный исследовательской группой Барбары Лисков в Массачусетском технологическом институте, имел понятие «где», которое позволяет вам указывать требования типа, над которым вы параметризуетесь. (Это похоже на то, что C ++ пытается сделать с Концепциями .)

Документ, описывающий дженерики Тора и как они взаимодействуют с подтипами Тора: Day, M; Грубер, R; Лисков, Б; Майерс, AC: предложения « Подтипы и где»: ограничение параметрического полиморфизма , ACM Conf для Obj-Oriented Prog, Sys, Lang и Apps , (OOPSLA-10): 156-158, 1995.

Я полагаю, что они, в свою очередь, основаны на работе, которая была проделана на Изумруде в конце 1980-х годов. (Я не читал эту работу, но ссылка: Black, A; Hutchinson, N; Jul, E; Levy, H; Carter, L: Распределение и абстрактные типы в Emerald , _IEEE T. Software Eng., 13 ( 1): 65-76, 1987.

И Тор, и Изумруд были «академическими языками», поэтому они, вероятно, не получили достаточного использования, чтобы люди действительно могли понять, действительно ли пункты (концепции) действительно решают какие-либо реальные проблемы. Интересно прочитать статью Бьярна Страуструпа о том, почему первая попытка использования Concepts в C ++ не удалась: Stroustrup, B: Решение C ++ 0x «Удалить концепции» , доктор Доббс , 22 июля 2009 г. (Дополнительная информация на домашней странице Страуструпа . )

Другое направление, которое люди, кажется, пробуют - это то, что называется чертами . Например, язык программирования Mozilla Rust использует черты. Насколько я понимаю (что может быть совершенно неправильно), объявлять, что класс удовлетворяет признаку, очень похоже на то, что класс реализует интерфейс, но вы говорите «ведет себя как», а не как «является». Похоже, что новые языки программирования Swift от Apple используют аналогичную концепцию протоколов для определения ограничений параметров для обобщений .

Блуждающая логика
источник
Черты ржавчины похожи на классы типа Haskell. Но есть два угла, на которые можно взглянуть: 1. Рассматривайте это как средство установления ограничений. 2. Определите универсальный интерфейс, который может быть связан с конкретным типом или универсальным классом типов.
BitTickler