Какой компромисс для вывода типа?

29

Кажется, что все новые языки программирования или, по крайней мере, те, которые стали популярными, используют вывод типов. Даже Javascript получил типы и вывод типов через различные реализации (Acscript, typcript и т. Д.). Это выглядит великолепно для меня, но мне интересно, есть ли какие-то компромиссы или почему, скажем, Java или старые добрые языки не имеют вывода типа

  • При объявлении переменной в Go без указания ее типа (с использованием var без типа или синтаксиса: =) тип переменной выводится из значения в правой части.
  • D позволяет писать большие фрагменты кода без избыточного указания типов, как это делают динамические языки. С другой стороны, статический вывод выводит типы и другие свойства кода, предоставляя лучшее из статического и динамического миров.
  • Механизм вывода типов в Rust довольно умный. Он делает больше, чем просто смотрит на тип значения r во время инициализации. Также выглядит, как переменная впоследствии используется для определения ее типа.
  • Swift использует вывод типа для разработки подходящего типа. Вывод типа позволяет компилятору автоматически определять тип определенного выражения при компиляции вашего кода, просто проверяя предоставленные вами значения.
Пользователь без шляпы
источник
3
В C # общие рекомендации гласят, что не всегда следует использовать, varпотому что это иногда может ухудшить читабельность
Мефи
5
«... или почему, скажем, Java или старые добрые языки не имеют вывода типов» Причины, вероятно, исторические; ML появился через 1 год после C, согласно Википедии, и имел типовое заключение. Java пыталась обратиться к разработчикам на C ++ . C ++ начинался как расширение C, и основными задачами C были переносимая оболочка над сборкой и простота компиляции. Что бы это ни стоило, я читал, что подтипирование делает вывод типов неразрешимым и в общем случае.
Довал
3
@Doval Scala, кажется, неплохо справляется с выводом типов для языка, который поддерживает наследование подтипов. Он не так хорош, как любой из языков семейства ML, но, вероятно, настолько хорош, насколько вы можете просить, учитывая дизайн языка.
KChaloux
12
Стоит провести различие между выводом типа (однонаправленный, как C # varи C ++ auto) и выводом типа (двунаправленный, как Haskell let). В первом случае тип имени может быть выведен только из его инициализатора - его использование должно соответствовать типу имени. В последнем случае тип имени также может быть выведен из его использования - что полезно в том смысле, что вы можете написать просто []для пустой последовательности, независимо от типа элемента, или newEmptyMVarдля новой пустой переменной, независимо от референта. тип.
Джон Перди
Вывод типа может быть невероятно сложным для реализации, особенно эффективно. Люди не хотят, чтобы их сложность компиляции страдала от экспоненциального разрушения от более сложных выражений. Интересное чтение: cocoawithlove.com/blog/2016/07/12/type-checker-issues.html
Александр - Восстановить Монику

Ответы:

43

Система типов в Haskell полностью незаметна (оставляя в стороне полиморфную рекурсию, некоторые расширения языка и устрашающее ограничение мономорфизма ), однако программисты по-прежнему часто предоставляют аннотации типов в исходном коде, даже когда они не нужны. Зачем?

  1. Тип аннотации служат документацией . Это особенно верно с типами, такими же выразительными, как у Haskell. Учитывая имя функции и ее тип, вы обычно можете довольно точно догадаться, что делает функция, особенно когда функция параметрически полиморфна.
  2. Тип аннотации могут стимулировать разработку . Написание сигнатуры типа перед тем, как писать тело функции, похоже на разработку через тестирование. По моему опыту, когда вы компилируете функцию на Haskell, она обычно работает в первый раз. (Конечно, это не избавляет от необходимости автоматизированных тестов!)
  3. Явные типы могут помочь вам проверить ваши предположения . Когда я пытаюсь понять некоторый код, который уже работает, я часто добавляю к нему то, что я считаю правильными аннотациями типов. Если код все еще компилируется, я знаю, что понял. Если это не так, я прочитал сообщение об ошибке.
  4. Сигнатуры типов позволяют вам специализировать полиморфные функции . Очень редко API является более выразительным или полезным, если определенные функции не являются полиморфными. Компилятор не будет жаловаться, если вы дадите функции менее общий тип, чем было бы выведено. Классический пример map :: (a -> b) -> [a] -> [b]. Его более общая форма ( fmap :: Functor f => (a -> b) -> f a -> f b) применяется ко всем Functors, а не только к спискам. Но считалось, что mapэто будет легче понять новичкам, поэтому он живет вместе со своим старшим братом.

В целом, недостатки статически типизированной, но выводимой системы во многом совпадают с недостатками статической типизации в целом, изношенное обсуждение этого сайта и других («Недостатки статической типизации в поиске» принесут вам сотни страниц пламенных войн). Конечно, некоторые из указанных недостатков улучшаются благодаря меньшему количеству типовых аннотаций в выводимой системе. Кроме того, вывод типов имеет свои преимущества: разработка с использованием дырок невозможна без вывода типов.

Java * доказывает, что язык, требующий слишком большого количества аннотаций типов, становится раздражающим, но слишком мало вы теряете преимущества, описанные выше. Языки с логическим выводом типа «отказ от участия» обеспечивают приемлемый баланс между двумя крайностями.

* Даже Java, этот великий козел отпущения, выполняет определенное количество локальных выводов типов. В таком выражении, как Map<String, Integer> = new HashMap<>();, вам не нужно указывать универсальный тип конструктора. С другой стороны, языки в стиле ML, как правило, являются глобально выводимыми.

Бенджамин Ходжсон
источник
2
Но вы не новичок;) Вы должны иметь представление о типах классов и видов, прежде чем вы действительно "получите" Functor. Списки и map, скорее всего, знакомы для неопытных Хаскеллеров.
Бенджамин Ходжсон
1
Считаете ли вы C # varпримером вывода типа?
Бенджамин Ходжсон
1
Да, C # var- правильный пример.
Доваль
1
Сайт уже помечает это обсуждение как слишком длинное, так что это будет мой последний ответ. В первом случае компилятор должен определить тип x; в последнем нет типа для определения, все типы известны, и вам просто нужно проверить, что выражение имеет смысл. Разница становится более важной, когда вы переходите от простых примеров и xиспользуется в нескольких местах; Затем компилятор должен перепроверить места, где xон используется для определения 1) можно ли назначить xтип так, чтобы код проверял тип? 2) Если это так, какой самый общий тип мы можем назначить?
Доваль
1
Обратите внимание, что new HashMap<>();синтаксис был добавлен только в Java 7, а лямбда-выражения в Java 8 допускают довольно много «реальных» выводов типа.
Майкл Боргвардт
8

В C # вывод типов происходит во время компиляции, поэтому стоимость времени выполнения равна нулю.

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

var s = new SomeReallyLongTypeNameWith<Several, Type, Parameters>(andFormal, parameters);

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

Используйте фактическое имя типа, когда оно явное, улучшает ясность кода.

В некоторых ситуациях невозможно использовать определение типа, например, объявления переменных-членов, значения которых установлены во время построения, или когда вы действительно хотите, чтобы intellisense работал правильно (IDE Hackerrank не будет intellisense для членов переменной, если вы не объявите тип явно) ,

Роберт Харви
источник
16
«В C # вывод типов происходит во время компиляции, поэтому стоимость времени выполнения равна нулю». Вывод типа происходит во время компиляции по определению, так обстоит дело в каждом языке.
Доваль
Тем лучше.
Роберт Харви
1
@Doval можно сделать вывод типа во время JIT-компиляции, которая явно имеет стоимость времени выполнения.
Жюль
6
@Jules Если вы дошли до запуска программы, значит она уже проверена на тип; нечего выводить. То, о чем вы говорите, обычно не называется выводом типа, хотя я не уверен, каков правильный термин.
Доваль
7

Хороший вопрос!

  1. Поскольку тип явно не аннотирован, он может иногда усложнять чтение кода, что приводит к большему количеству ошибок. Правильное использование, конечно, делает код чище и более читабельным. Если вы пессимист , и думаете , что большинство программистов плохо (или работы , где большинство программистов являются плохо), то это будет чистый убыток.
  2. Хотя алгоритм вывода типов относительно прост, он не является бесплатным . Такие вещи немного увеличивают время компиляции.
  3. Поскольку тип явно не аннотирован, ваша IDE также не может угадать, что вы пытаетесь сделать, нанося вред автозаполнению и аналогичным помощникам во время процесса объявления.
  4. В сочетании с перегрузками функций вы можете иногда попадать в ситуации, когда алгоритм вывода типов не может решить, какой путь выбрать, что приводит к более уродливым аннотациям стиля приведения. (Это случается, например, с синтаксисом анонимной функции C #).

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

Telastyn
источник
1
автозаполнение не имеет никакого значения для вывода типа. Неважно, как был определен тип, только то, что имеет тип. И проблемы с лямбдами C # не имеют ничего общего с логическим выводом, они связаны с тем, что лямбды полиморфны, но система типов не может справиться с этим, что никак не связано с логическим выводом.
DeadMG
2
@deadmg - конечно, после того, как переменная объявлена, у нее нет проблем, но пока вы печатаете в объявлении переменной, она не может сузить параметры правой части до объявленного типа, поскольку объявленного типа нет.
Теластин
@deadmg - что касается лямбд, я не совсем понимаю, что вы имеете в виду, но я уверен, что это не совсем правильно, исходя из моего понимания того, как все работает. Даже что-то столь же простое, как и var foo = x => x;неудача, потому что язык должен выводить xздесь и дальше нечего. Когда лямбда-выражения создаются, они создаются как делегаты с явной типизацией, Func<int, int> foo = x => xвстроенные в CIL, как Func<int, int> foo = new Func<int, int>(x=>x);где лямбда- выражение создается как сгенерированная, явно типизированная функция. Для меня вывод типа xявляется неотъемлемой частью вывода типа ...
Теластин
3
@Telastyn Это неправда, что языку нечего продолжать. Вся информация, необходимая для ввода выражения x => x, содержится внутри самой лямбды - она ​​не ссылается ни на какие переменные из окружающей области видимости. Любой нормальный функциональный язык будет правильно сделать вывод , что его типа «для всех типов a, a -> a». Я думаю, что DeadMG пытается сказать, что в системе типов C # отсутствует свойство основного типа, что означает, что всегда можно определить наиболее общий тип для любого выражения. Это очень легко разрушить.
Доваль
@Doval - Enhif ... в C # нет такой вещи, как объекты универсальных функций, которые, я думаю, немного отличаются от того, о чем вы оба говорите, даже если это приводит к одной и той же проблеме.
Теластин
4

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

Ява оказывается скорее исключением, чем правилом. Даже C ++ (который, как я полагаю, квалифицируется как «старый добрый язык» :)) поддерживает вывод типа с autoключевым словом начиная со стандарта C ++ 11. Он работает не только с объявлением переменной, но и с типом возвращаемого значения функции, что особенно удобно для некоторых сложных шаблонных функций.

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

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

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

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

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

Габор Ангьял
источник
1

Основное различие между системой логического вывода типа Хиндли-Милнера и логическим типом логического вывода заключается в направлении потока информации. В HM информация типа передается вперед и назад через унификацию; в Go информация о типах передается только перенаправлениями: она рассчитывает только перестановки подстановок.

Вывод типа HM - это великолепная инновация, которая хорошо работает с функциональными языками с полиморфной типизацией, но авторы Go, вероятно, утверждают, что он пытается сделать слишком много:

  1. Тот факт, что информация передается как вперед, так и назад, означает, что вывод типа HM является очень нелокальным делом; в отсутствие аннотаций типов каждая строка кода в вашей программе может способствовать вводу одной строки кода. Если у вас есть только прямое замещение, вы знаете, что источником ошибки типа должен быть код, предшествующий вашей ошибке; не код, который идет после.

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

Эдвард З. Ян
источник
2
К сожалению, это выглядит как хороший ответ на другой вопрос. OP задает вопрос о «выводе типа Go» в сравнении с языками, которые вообще не имеют вывода о типе, а не о «Go-стиле» и HM.
Иксрек
-2

Это вредит читабельности.

сравнить

var stuff = someComplexFunction()

против

List<BusinessObject> stuff = someComplexFunction()

Это особенно проблема при чтении вне IDE, как на github.

Мигел
источник
4
это , кажется, не предлагает ничего существенного по точкам сделанных и разъяснено в предыдущих 6 ответов
комар
Если вы используете собственное имя вместо «сложного нечитабельного», то varможете использовать его без ущерба для читабельности. var objects = GetBusinessObjects();
Фабио
Хорошо, но тогда вы пропускаете систему типов в имена переменных. Это как венгерская нотация. После рефакторинга у вас больше шансов получить имена, которые не соответствуют коду. В целом, функциональный стиль заставляет вас терять естественные преимущества пространства имен, которые предоставляют классы. Таким образом, вы получите больше squareArea () вместо square.area ().
Мигель