Почему вывод типа полезен?

37

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

Компилятор выводит тип; Есть старые алгоритмы для этого. Но реальный вопрос заключается в том, почему я, программист, хочу выводить тип моих переменных при чтении кода? Разве не быстрее для кого-то просто читать тип, чем думать, какой тип существует?

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

m3th0dman
источник
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);
    }
}

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

Хотя вывод типа действительно полезен для локальных переменных, он не должен использоваться для открытых API, которые должны быть однозначно задокументированы. И иногда типы действительно важны для понимания того, что происходит в коде. В таких случаях было бы глупо полагаться только на вывод типа.

Есть много языков, которые поддерживают вывод типов. Например:

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

  • C #. Вы можете объявить переменные с помощью var, который запускает ограниченную форму вывода типа. Он по-прежнему управляет большинством случаев, когда требуется вывод типов. В некоторых местах вы можете полностью исключить тип (например, в лямбдах).

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

Амон
источник
13
Также обратите внимание, что в C # есть анонимные типы, то есть типы без имени, но в C # есть система именных типов, то есть система типов, основанная на именах. Без вывода типов эти типы никогда не смогут использоваться!
Йорг Миттаг,
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. Если бы я (key1, key2, value)извлек этот цикл в функцию, которая выдает Iterable из -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 И вы всегда можете найти этот тип довольно легко (большинство IDE скажут вам, и есть низкотехнологичное решение намеренно вызывать ошибку типа и позволить компилятору печатать фактический тип), это просто не так, поэтому не раздражает вас в общем случае.
4

Я думаю, что вывод типа довольно важен и должен поддерживаться на любом современном языке. Мы все развиваемся в IDE, и они могут очень помочь, если вы хотите узнать тип вывода, только немногие из нас взломают 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

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

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

jmoreno
источник
3
Технически, информация всегда избыточна, когда можно опустить подписи вручную, иначе компилятор не сможет их определить! Но я понимаю, что вы имеете в виду: когда вы просто дублируете подпись на нескольких точках в одном представлении, это действительно избыточно для мозга , в то время как несколько удачно расположенных типов дают информацию, которую вам придется искать долго, возможно, с много хороших неочевидных преобразований.
оставил около
@leftaroundabout: избыточно при чтении программистом.
Jmoreno
3

Предположим, кто-то видит код:

someBigLongGenericType variableName = someBigLongGenericType.someFactoryMethod();

Если someBigLongGenericTypeприсваивается из возвращаемого типа someFactoryMethod, насколько вероятно, что кто-то, читающий код, будет замечать, если типы не совпадают точно, и с какой готовностью кто-то, кто заметил несоответствие, может распознать, было ли это намеренным или нет?

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

Supercat
источник
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 ++ явно запрещает использование лямбда-типов, даже определение типов с использованием of typeofделает язык бесполезным. И это недостаток самого языка, который должен быть исправлен на мой взгляд.
Учитель