Чому корисний висновок типу?


37

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

Компілятор визначає тип; для цього існують старі алгоритми. Але справжнє питання полягає в тому, чому я, програміст, хотів би зробити висновок про тип своїх змінних, коли читаю код? Чи не швидше для когось просто прочитати тип, ніж думати, який тип існує?

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


5
На мій досвід, типи набагато важливіші при написанні коду, ніж його читання. Читаючи код, я шукаю алгоритми та конкретні блоки, до яких добре названі змінні зазвичай спрямовують мене. Мені дійсно не потрібно вводити контрольний код просто для того, щоб прочитати і зрозуміти, що він робить, якщо це страшенно погано написано. Однак, читаючи код, роздутий зайвими непотрібними деталями, які я не шукаю (як занадто багато анотацій типу), часто ускладнюється пошук бітів, які я шукаю. Висновок типу я б сказав, що це величезна користь для читання набагато більше, ніж написання коду.
Джиммі Хоффа

Як тільки я знайду фрагмент коду, який я шукаю, я можу почати перевіряти його, але в будь-який момент ви не повинні зосереджуватися на більш ніж 10-ти рядках коду, і в цей момент робити клопоту не потрібно зробіть висновок про себе, тому що ви подумки вибираєте цілий блок на початку, і, швидше за все, використовуєте інструменти, які допоможуть вам зробити це в будь-якому випадку. З'ясування типів 10 рядків коду, на який ви намагаєтеся зонувати, рідко займає багато часу, але це частина, де ви перейшли від читання до написання все одно, що так чи інакше зустрічається набагато рідше.
Джиммі Хоффа

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

2
Робота над першим моментом @JimmyHoffa, розгляньте читання взагалі. Чи легше розібратися і прочитати речення, не кажучи вже про його розуміння, зосередившись на частці мовлення окремих його слів? "Корова (стаття) корова (іменник-однина) перестрибнула (дієслово-минуле) через (прийменник) (стаття) місяць (іменник). (Пунктуація-період)".
Зев Шпіц

Відповіді:


46

Давайте подивимось на Java. У Java не може бути змінних із виведеними типами. Це означає, що мені часто доводиться викладати тип, навіть якщо людському читачеві абсолютно очевидно, що це за тип:

int x = 42;  // yes I see it's an int, because it's a bloody integer literal!

// Why the hell do I have to spell the name twice?
SomeObjectFactory<OtherObject> obj = new SomeObjectFactory<>();

І іноді просто роздратувати цілий тип.

// this code walks through all entries in an "(int, int) -> SomeObject" table
// represented as two nested maps
// Why are there more types than actual code?
for (Map.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>> row : table.entrySet()) {
    Integer rowKey = entry.getKey();
    Map<Integer, SomeObject<SomeObject, T>> rowValue = entry.getValue();
    for (Map.Entry<Integer, SomeObject<SomeObject, T>> col : rowValue.entrySet()) {
        Integer colKey = col.getKey();
        SomeObject<SomeObject, T> colValue = col.getValue();
        doSomethingWith<SomeObject<SomeObject, T>>(rowKey, colKey, colValue);
    }
}

Це багатослівне статичне введення заважає мені, програмісту. Більшість типових анотацій - це повторювані наповнювачі рядків, безресурсні регуляції того, що ми вже знаємо. Однак мені подобається статичне введення тексту, оскільки воно справді може допомогти у виявленні помилок, тому використання динамічного введення тексту не завжди є гарною відповіддю. Виведення типу є найкращим з обох світів: я можу опустити нерелевантні типи, але все ж бути впевненим, що моя програма (тип) перевіряється.

Хоча висновок типу дійсно корисний для локальних змінних, він не повинен використовуватися для публічних API, які повинні бути однозначно задокументовані. І іноді типи дійсно є критичними для розуміння того, що відбувається в коді. У таких випадках було б нерозумно покладатися лише на умови виводу.

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

  • C ++. У autoключових словах типу тригерів умовиводи. Без цього написання типів для лямбда або для записів у контейнерах було б пеклом.

  • C #. Ви можете оголосити змінні за допомогою var, що запускає обмежену форму виводу типу. Він все ще керує більшістю випадків, коли потрібно зробити висновок. У певних місцях ви можете повністю залишити тип (наприклад, у лямбдах).

  • Haskell та будь-яку мову в сім'ї ML. Хоча специфічний аромат виводу типу, який використовується тут, є досить потужним, ви все ще часто бачите анотації типу для функцій, і з двох причин: Перша - це документація, а друга - перевірка того, що умовивід типу фактично знайшов очікувані типи. Якщо є розбіжність, ймовірно, є якась помилка.


13
Зауважте також, що C # має анонімні типи, тобто типи без імені, але C # має систему номінального типу, тобто систему типів на основі імен. Без виводу типу ці типи ніколи не можна використовувати!
Йорг W Міттаг

10
Деякі приклади, на мою думку, трохи надумані. Ініціалізація до 42 не означає автоматично, що змінна є int, вона може бути будь-якого числового типу, включаючи парне char. Також я не бачу, чому ви хочете прописати весь тип для того, Entryколи ви можете просто ввести ім'я класу та дозволити вашому IDE зробити необхідний імпорт. Єдиний випадок, коли вам потрібно прописати ціле ім'я, це коли у вас є власний клас з тим самим іменем. Але мені все одно здається поганим дизайном.
Малькольм

10
@Malcolm Так, всі мої приклади надумані. Вони служать для ілюстрації точки. Коли я писав intприклад, я думав про (на мій погляд, досить розумну поведінку) більшості мов, які мають умовивідведення типу. Зазвичай вони роблять висновок про те, intабо Integerяк воно називається цією мовою. Краса виводу типу полягає в тому, що це завжди необов’язково; ви все одно можете вказати інший тип, якщо він потрібен. Щодо Entryприкладу: хороший момент, я заміню його на Map.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>>. У Java навіть немає псевдонімів типу :(
amon

4
@ m3th0dman Якщо тип важливий для розуміння, то ви все одно можете його чітко згадати. Вибір типу завжди необов’язковий. Але тут тип colKeyі очевидний, і неактуальний: ми дбаємо лише про те, щоб він був відповідним як другий аргумент doSomethingWith. Якби я витягнув цю петлю у функцію, яка дає Iterable of (key1, key2, value)-triples, найбільш загальною була б підпис <K1, K2, V> Iterable<TableEntry<K1, K2, V>> flattenTable(Map<K1, Map<K2, V>> table). Всередині цієї функції реальний тип colKey( Integer, не K2) абсолютно не має значення.
амон

4
@ m3th0dman - це досить широкий роздум про те, що " більшість " кодів є тим чи іншим . Анекдотична статистика. Там, безумовно , немає сенсу писати тип двічі в ініціалізатор: View.OnClickListener listener = new View.OnClickListener(). Ти б все-таки знав тип, навіть якби програміст був "ледачий" і скоротив його var listener = new View.OnClickListener(якщо це було можливо). Така надмірність є загальним - Я не буду ризикувати здогад тут - і видалення його дійсно походить від думок про майбутнє читачів. Кожну мовну функцію слід використовувати обережно, я не сумніваюся в цьому.
Конрад Моравський

26

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

І, як мій досвід, часто точний тип не є важливим. Ви , звичайно , іноді гніздяться вираження: x + y * z, monkey.eat(bananas.get(i)), factory.makeCar().drive(). Кожен із них містить під вирази, які оцінюють до значення, тип якого не виписаний. І все-таки вони абсолютно чіткі. Ми все гаразд залишаємо тип невстановленим, оскільки це досить легко з'ясувати з контексту, і його написання принесло б більше шкоди, ніж користі (захаращувати розуміння потоку даних, займати цінний екран та короткострокову пам'ять).

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

user = db.get_poster(request.post['answer'])
name = db.get_display_name(user)

Чи має значення userоб’єкт сутності, ціле число, рядок чи щось інше? Для більшості цілей це не так, достатньо знати, що він представляє користувача, походить від HTTP-запиту і використовується для отримання імені для відображення в правому нижньому куті відповіді.

І коли це має значення, автор вільний виписати тип. Це свобода, яку потрібно використовувати відповідально, але те саме стосується всього іншого, що може підвищити читабельність (назви змінних та функцій, форматування, дизайн API, пробіл). Дійсно, конвенція Haskell і ML (де все можна зробити без додаткових зусиль) полягає у виписуванні типів функцій не локальних функцій, а також локальних змінних та функцій, коли це доречно. Тільки новачки дозволяють зробити висновок про кожен тип.


2
+1 Це має бути прийнятою відповіддю. Це іде саме до того, чому висновок про тип - чудова ідея.
Крістіан Хейтер

Точний тип userмає значення, якщо ви намагаєтесь розширити функцію, оскільки вона визначає, що ви можете зробити з user. Це важливо, якщо ви хочете додати перевірку правильності (наприклад, через вразливість безпеки), або забули, що вам потрібно щось робити з користувачем, а не просто показувати його. Щоправда, такі типи читання для розширення рідше, ніж просто читання коду, але вони також є важливою частиною нашої роботи.
cmaster

@cmaster І ви завжди можете знайти цей тип досить легко (більшість IDE скажуть вам, і є низькотехнологічне рішення про навмисне спричинення помилки типу та надання компілятору друку фактичного типу), це просто поза дорогою, так що не дратує вас у загальній справі.

4

Я думаю, що умовивід типу є досить важливим і його слід підтримувати будь-якою сучасною мовою. Всі ми розвиваємося в ІДЕ, і вони могли б дуже допомогти у випадку, якщо ви хочете дізнатися про виведений тип, мало хто з нас вилазить vi. Придумайте, наприклад, багатослівність та церемоніальний код на Java.

  Map<String,HashMap<String,String>> map = getMap();

Але ви можете сказати, що це добре, що мені допоможе IDE, це може бути достовірним моментом. Однак деякі функції не були б без допомоги виводу типу, наприклад, анонімних типів C #.

 var person = new {Name="John Smith", Age = 105};

Linq не буде так добре , як зараз , без допомоги умовиводів типу, Selectнаприклад ,

  var result = list.Select(c=> new {Name = c.Name.ToUpper(), Age = c.DOB - CurrentDate});

Цей анонімний тип буде чітко виведений на змінну.

Мені не подобається висновок про типи повернення, Scalaтому що я думаю, що ваша точка стосується тут, нам повинно бути зрозуміло, яка функція повертається, щоб ми могли більш ефективно використовувати API


Map<String,HashMap<String,String>>? Звичайно, якщо ти не використовуєш типи, то їх написання мало користі. Table<User, File, String>однак є більш інформативним, і в його написанні є користь.
MikeFHay

4

Я думаю, що відповідь на це дійсно проста: це економить читання та записування зайвої інформації. Особливо в об'єктно-орієнтованих мовах, де у вас є тип з обох сторін знаку рівності.

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


3
Ну, технічно, інформація завжди є зайвою, коли можливо пропустити підписи вручну: інакше компілятор не міг би їх зробити! Але я розумію, що ви маєте на увазі: коли ви просто дублюєте підпис на декількох плямах в одному огляді, це дійсно зайве для мозку , тоді як кілька добре розміщених типів дають інформацію, яку вам доведеться довго шукати, можливо, за допомогою добре багато неочевидних перетворень.
Ліворуч близько

@leftaroundabout: надлишковий при читанні програмістом.
jmoreno

3

Припустимо, хто бачить код:

someBigLongGenericType variableName = someBigLongGenericType.someFactoryMethod();

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

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


2

Я бачу, що вже є ряд тонких відповідей. Деякі з яких я повторюю, але іноді ви просто хочете викласти речі своїми словами. Я прокоментую кілька прикладів із C ++, оскільки це мова, з якою я найбільше знайомий.

Те, що потрібно, ніколи не є нерозумним. Вибір типу необхідний, щоб інші функції мови були практичними. У C ++ можна мати невідтворювані типи.

struct {
    double x, y;
} p0 = { 0.0, 0.0 };
// there is no name for the type of p0
auto p1 = p0;

C ++ 11 додали лямбда, які також не піддаються інтеграції.

auto sq = [](int x) {
    return x * x;
};
// there is no name for the type of sq

Введіть також умовиводи, що лежать в основі шаблонів.

template <class x_t>
auto sq(x_t const& x)
{
    return x * x;
}
// x_t is not known until it is inferred from an expression
sq(2); // x_t is int
sq(2.0); // x_t is double

Але ваші запитання були "чому я, програміст, хотів зробити висновок про тип моїх змінних, коли я читаю код? Хіба ніхто не швидше прочитати тип, ніж думати, який тип є?"

Тип умовиводу видаляє надмірність. Що стосується читання коду, іноді може бути швидше і простіше мати зайву інформацію в коді, але надмірність може затьмарити корисну інформацію . Наприклад:

std::vector<int> v;
std::vector<int>::iterator i = v.begin();

Не потрібно багато знайомого зі стандартною бібліотекою для програміста C ++, щоб визначити, що я є ітератором, i = v.begin()тому явне оголошення типу має обмежене значення. Своєю присутністю він затьмарює більш важливі деталі (наприклад, які iвказують на початок вектора). Тонка відповідь @amon дає ще кращий приклад багатослівності, що затьмарює важливі деталі. На відміну від використання умовиводу типу надає більшу увагу важливим деталям.

std::vector<int> v;
auto i = v.begin();

Хоча читання коду важливо, це недостатньо, в якийсь момент вам доведеться припинити читання і почати писати новий код. Надмірність у коді робить модифікацію коду повільнішою та жорсткішою. Наприклад, скажіть, що у мене є такий фрагмент коду:

std::vector<int> v;
std::vector<int>::iterator i = v.begin();

У випадку, якщо мені потрібно змінити тип значення вектора, щоб подвоїти зміну коду на:

std::vector<double> v;
std::vector<double>::iterator i = v.begin();

У цьому випадку я повинен змінити код у двох місцях. Контраст із висновком типу, де початковий код:

std::vector<int> v;
auto i = v.begin();

І модифікований код:

std::vector<double> v;
auto i = v.begin();

Зауважте, що зараз мені потрібно змінити лише один рядок коду. Екстраполюйте це на велику програму, а умовивід може поширювати зміни на типи набагато швидше, ніж ви можете з редактором.

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

int pi = 3.14159;

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

int y = sq(x);

У випадку, коли sq(x)повертається int, не очевидно, чи yє це intтому, що це тип повернення sq(x)або тому, що він відповідає операторам, які використовують y. Якщо я зміню інший код таким, який sq(x)більше не повертається int, то з цього рядка невідомо, чи yслід оновити тип . Контраст з тим же кодом, але з використанням умовиводу типу:

auto y = sq(x);

У цьому намірі зрозумілі, вони yповинні бути того ж типу, що і повернуті sq(x). Коли код змінює тип повернення sq(x), тип yзмін автоматично відповідає.

У C ++ є друга причина, чому вищенаведений приклад простіший з висновком типу, умовивід типу не може ввести неявне перетворення типу. Якщо тип повернення sq(x)відсутній int, компілятор з мовчки вставляє неявне перетворення в int. Якщо тип повернення sq(x)- тип складного типу, який визначає operator int(), цей прихований функція виклику може бути довільно складним.


Непоганий момент про непереборні типи в C ++. Однак я думаю, що це менше причин для додавання виводу типу, ніж для виправлення мови. У першому представленому вами випадку програмісту потрібно було б просто дати ім’я, щоб уникнути використання виводу типу, тому це не є вагомим прикладом. Другий приклад є сильним лише тому, що C ++ явно забороняє бути лямбда-типами безперервними, навіть набір тексту з використанням не typeofдає мови марним. І це, на мою думку, дефіцит самої мови.
cmaster
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.