У чому полягає компроміс для виводу типу?


29

Здається, що всі нові мови програмування або, принаймні, ті, що стали популярними, використовують умовивід. Навіть Javascript отримав типи та умови виводу, хоча різні реалізації (Acscript, typecript тощо). Мені це здається чудово, але мені цікаво, чи є компроміси чи чому, скажімо, Java або старі добрі мови не мають висновку про тип

  • При оголошенні змінної в Go, не вказуючи її тип (використовуючи var без типу або синтаксис: =), тип змінної виводиться зі значення праворуч.
  • D дозволяє писати великі фрагменти коду без надмірного введення типів, як це роблять динамічні мови. З іншого боку, статичний висновок виводить типи та інші властивості коду, надаючи найкраще як статичному, так і динамічному світам.
  • Двигун виводу типу в Rust досить розумний. Це більше, ніж дивитися на тип r-значення під час ініціалізації. Він також виглядає, як змінна використовується згодом для висновку про її тип.
  • Swift використовує умовивід типу для опрацювання відповідного типу. Вибір типу дозволяє компілятору автоматично виводити тип певного виразу, коли він компілює ваш код, просто досліджуючи надані вами значення.

3
У C #, загальні принципи говорять не завжди використовувати , varтому що іноді це може пошкодити читання.
Мефі

5
"... або чому, скажімо, Java або старі добрі мови не мають висновку про тип" Причини, ймовірно, історичні; Згідно з Вікіпедією, ML з'явився через 1 рік після С, і він мав висновок про тип. Java намагалася звернутися до розробників C ++ . C ++ розпочався як розширення C, а основними проблемами C були - переносний обгортку над складанням та його легко компілювати. Що б там не було, я читав, що підтипізація робить висновок про тип нерозбірливим і в загальному випадку.
Doval

3
@Doval Scala, схоже, робить непогану роботу при виведенні типів для мови, яка підтримує спадкове підтип. Це не так добре, як будь-які мови сімейства ML, але, мабуть, так добре, як ви могли б попросити, враховуючи дизайн мови.
KChaloux

12
Варто провести різницю між типом дедукції (мононаправленим, як C # varі C ++ auto) і типом виводу (двонаправленим, як Haskell let). У першому випадку тип імені може бути виведений лише з його ініціалізатора - його використання повинно відповідати типу імені. В останньому випадку тип імені може бути встановлений також із його використання - що корисно тим, що ви можете писати просто []для порожньої послідовності, незалежно від типу елемента, або newEmptyMVarдля нової нульової змінної посилання незалежно від референта тип.
Джон Перді

Висновок типу може бути надзвичайно складним у виконанні, особливо ефективно. Люди не хочуть, щоб їх складнощі під час компіляції зазнавали експоненціального вибуху від складніших виразів. Цікаво читати: cocoawithlove.com/blog/2016/07/12/type-checker-isissue.html
Олександр -

Відповіді:


43

Система типу Хаскелла є повністю невідповідною (залишаючи осторонь поліморфну ​​рекурсію, певні розширення мови та жахливе обмеження мономорфізму ), але програмісти все ще часто надають анотації типів у вихідному коді, навіть коли цього не потрібно. Чому?

  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є правильним прикладом.
Doval

1
Сайт вже відзначає цю дискусію як занадто тривалу, тому це буде моєю останньою відповіддю. У першому компілятор повинен визначити тип x; в останньому немає типу для визначення, всі типи відомі, і вам просто потрібно перевірити, чи має вираз сенс. Різниця стає важливішою, коли ви переходите через тривіальні приклади та xвикористовуєтесь у кількох місцях; потім компілятор повинен перехресно перевірити місця, де xвикористовується для визначення 1) чи можна призначити xтип такий, що код буде вводити перевірку? 2) Якщо це так, то який найзагальніший тип ми можемо призначити?
Doval

1
Зауважте, що new HashMap<>();синтаксис був доданий лише в Java 7, а лямбди в Java 8 дозволяють отримати досить багато "реальних" висновків типу.
Майкл Боргвардт

8

У C # виведення типу відбувається під час компіляції, тому вартість виконання дорівнює нулю.

Що стосується стилю, varвикористовується для ситуацій, коли вручну вказувати тип незручно або непотрібно. Linq - одна з таких ситуацій. Інше:

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

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

Використовуйте власне ім’я типу, коли явне покращує чіткість коду.

Існують деякі ситуації, коли умовивод типу не може бути використаний, наприклад, декларації змінних членів, значення яких встановлені під час створення або коли ви дійсно хочете, щоб intellisense працювала належним чином (ID ID Hackerrank не буде інтелігувати членів змінної, якщо ви чітко не заявите тип) .


16
"У C #, висновок типу відбувається під час компіляції, тому вартість виконання дорівнює нулю." Висновок типу відбувається в час компіляції за визначенням, тому так є у всіх мовах.
Doval

Чим краще, тим краще.
Роберт Харві

1
@Doval можна зробити висновок типу під час компіляції JIT, що, очевидно, має витрати на виконання.
Жуль

6
@Jules Якщо ви довелись до запуску програми, то вона вже перевірена; нічого не можна зробити. Те, про що ви говорите, зазвичай не називається висновком типу, хоча я не впевнений, що таке правильний термін.
Doval

7

Гарне питання!

  1. Оскільки тип не позначається явно, він може часом ускладнювати читання коду - що призводить до появи більше помилок. При правильному використанні це звичайно робить чистий код і більш читабельним. Якщо ви песиміст , і думаєте , що більшість програмістів погано (або роботи , де більшість програмістів є погано), то це буде чистий збиток.
  2. Хоча алгоритм виводу типу порівняно простий, він не є безкоштовним . Такі речі дещо збільшують час складання.
  3. Оскільки тип не є явно анотованим, ваш IDE також не може здогадатися, що ви намагаєтесь зробити, завдаючи шкоди автозаповненню та подібним помічникам під час декларації.
  4. У поєднанні з перевантаженнями функцій ви можете час від часу потрапляти в ситуації, коли алгоритм виводу типу не може визначитися, який шлях пройти, що призводить до більш потворних анотацій стилю кастингу. (Це відбувається зовсім небагато, наприклад, з синтаксисом анонімних функцій C #).

Є ще езотеричні мови, які не можуть здійснити свою дивність без явного анотації типу. Поки немає жодного, про який я знаю, що є загальним / популярним / досить перспективним, щоб згадати, окрім як мимохідь.


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

@Doval - enh ... у C # немає такого поняття, як об'єкти загальної функції, які, на мою думку, є дещо різними, ніж те, про що ви обидва говорите, навіть якщо це призводить до однакових проблем.
Теластин

4

Мені це здається чудово, але мені цікаво, чи є компроміси чи чому, скажімо, Java або старі добрі мови не мають висновку про тип

Java, швидше, є винятком, ніж правилом. Навіть C ++ (який, на мою думку, кваліфікується як "стара стара мова" :)) підтримує тип висновку з autoключовим словом, оскільки стандарт C ++ 11. Він працює не тільки зі змінним оголошенням, але також як тип повернення функції, що особливо зручно для деяких складних функцій шаблону.

Неявне введення тексту та умови виводу мають багато корисних випадків використання, а також є деякі випадки використання, коли ви дійсно не повинні цього робити. Іноді це питання смаку, а також предмет дискусій.

Але те, що, безсумнівно, є добрі випадки використання, саме по собі є законною причиною для будь-якої мови його впровадження. Це також не важко реалізувати функцію, не виконувати покарання та не впливає суттєво на час компіляції.

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

Деякі відповідачі пояснюють, наскільки явний введення тексту іноді добре, і вони, безумовно, праві. Але непідтримка неявного введення означає, що мова весь час виконує явне введення тексту.

Таким чином, справжній зворотний рух полягає в тому випадку, коли мова не підтримує неявне введення тексту, тому що при цьому він стверджує, що розробник не має законних причин використовувати його, що не відповідає дійсності.


1

Основне відмінність між системою виводу типу Гіндлі-Мілнера та висновком типу Го-го - напрямок потоку інформації. У HM введіть інформацію, яка рухається вперед і назад шляхом об'єднання; У програмі Go введіть інформацію лише вперед: вона обчислює лише заміну.

Висновок типу HM - чудова інновація, яка добре працює з поліморфно типованими функціональними мовами, але автори Go, напевно, стверджують, що вона намагається зробити занадто багато:

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

  2. З висновком типу HM ви схильні думати з обмеженнями: коли ви використовуєте тип, ви обмежуєте, які можливі типи він може мати. Зрештою, можуть бути деякі змінні типу, які залишаються абсолютно не обмеженими. Розуміння виводу типу HM полягає в тому, що ці типи дійсно не мають значення, і тому вони перетворюються на поліморфні змінні. Однак цей додатковий поліморфізм може бути небажаним з кількох причин. По-перше, як зазначають деякі, цей додатковий поліморфізм може бути небажаним: НМ укладає фальшивий, поліморфний тип для якогось фальшивого коду, а згодом призводить до дивних помилок. По-друге, коли тип залишається поліморфним, це може мати наслідки для поведінки під час виконання. Наприклад, надмірно поліморфний проміжний результат є причиною того, що 'показують. read 'вважається неоднозначним у Хаскеллі; як інший приклад,


2
На жаль, це виглядає як хороша відповідь на інше питання. ОП запитує про "умови вибору типу" в порівнянні з мовами, які взагалі не мають жодного виводу, а не "стиль переходу" проти HM.
Іксрек

-2

Це шкодить читабельності.

Порівняйте

var stuff = someComplexFunction()

проти

List<BusinessObject> stuff = someComplexFunction()

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


4
це, здається, не пропонує нічого суттєвого щодо пунктів, викладених та пояснених у попередніх 6 відповідях
гнат

Якщо ви використовуєте власне ім'я замість "складного нечитабельного", то varйого можна використовувати, не завдаючи шкоди читанні. var objects = GetBusinessObjects();
Фабіо

Гаразд, але тоді ви просочуєте систему типу в імена змінних. Це як угорське позначення. Після рефакторингу ви, швидше за все, отримаєте імена, які не відповідають коду. Загалом, функціональний стиль підштовхує вас до втрати природних переваг простору імен, які надають класи. Таким чином, ви отримуєте більше squareArea () замість square.area ().
Мігель
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.