Ликбез по типизации в языках программирования

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

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

Советую изначально ознакомиться с сокращенным вариантом статьи, а после при желании приступить к развёрнутой версии.

Сокращенная вариация

Обычно языки программирования по типизации  делят на две крупные стороны: типизированные и нетипизированные (бестиповые). К первой стороне можно отнести C, Python, Scala, PHP и Lua, а ко второй язык ассемблера, Forth и Brainfuck.

Поскольку «бестиповая типизация» в сущности – чрезмерно проста, то в дальнейшем она не разделяется на другие типы. Вместо этого типизированные языки представляют собой ещё несколько видов:

  • Статическая типизация формируется из того, что окончательные типы переменных и функций складываются на этапе сбора данных, то есть в этот момент компилятор убеждён на 100%, какой тип, где находится.
  • Динамическая типизация складывается из типов, которые проявляются в течение проведения программы.

Примеры:

Статическая: C, Java, C#;

Динамическая: Python, JavaScript, Ruby.

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

Примеры:

Сильная: Java, Python, Haskell, Lisp;

Слабая: C, JavaScript, Visual Basic, PHP.

Явная типизация – языки данной типизации разнятся тем, что тип новых переменных / функций / их доводов необходимо задавать явно.

Неявная типизация – языки этой типизации переносят эту задачу на компилятор / интерпретатор.

Примеры:

Явная: C++, D, C#

Неявная: PHP, Lua, JavaScript

Вдобавок следует отметить, что все подобные типы переплетаются , к примеру язык C обладает статистической слабой явной типизацией, а язык Python — динамической сильной неявной.

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

Развернутая вариация

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

Бестиповая типизация

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

Такая типизация свойственна низкоуровневым (язык ассемблера, Forth) и эзотерическим (Brainfuck, HQ9, Piet) языкам. Но и у неё наравне с недочётами, присутствует перечень достоинств.

Достоинства

  • Даёт возможность писать на максимально низком уровне, более того компилятор / интерпретатор не будет препятствовать никакими проверками категорий. Вам предоставляется возможность осуществлять различные операции над любыми видами данных.
  • Получаемый код чаще всего наиболее действенен.
  • Прозрачность инструкций. Несомненно, зная язык, мы можем представить, как выглядит код.

Недостатки

  • Трудность. Зачастую становится необходимо предоставление всесторонних значений, подобных строкам, спискам, структурам. С этим бывают недочеты.
  • Нехватка контроля. Всякие нецелесообразные поступки, к примеру, вычитание указателя на массив из символа будут являться абсолютно разумными, что приведёт к практически незаметным ошибкам.
  • Пониженная степень отвлеченности. Рабочий процесс со всяким тяжёлым видом данных не отличается от работы с числами, что создаёт различные сложности.

Сильная бестиповая типизация?

Да, такое наблюдается. К примеру, в языке ассемблера (для архитектуры х86/х86-64, других не знаю) не следует ассемблировать программу, в случае если вы попробуете загрузить в регистр cx (16 бит) данные из регистра rax (64 бита).

mov cx, eax ; ошибка времени ассемблирования.
То есть выходит, что в ассемблере все-таки есть типизация? Я думаю, что количество подобных проверок неудовлетворительно. Но Ваше мнение зависит, исключительно, только от Вас.

Статическая и динамическая типизации

типизация

Важно, что статическую (static) типизацию отличает от динамической (dynamic) именно то, что все проверки видов происходят на этапе сбора данных, а не на этапе осуществления.

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

Преимущества статической типизации

Проверки типов осуществляются исключительно один раз — на этапе сбора данных. А это означает, что нам не требуется постоянно определять, не пытаемся ли мы поделить число на строку (и либо выдать ошибку, либо осуществить преобразование).

Скорость проведения. Из предыдущего пункта ясно, что статически типизированные языки чаще всего быстрее динамически типизированных.

При неких дополнительных условиях, предоставляется возможность выявлять потенциальные ошибки уже на этапе компиляции.

Ускорение разработки при поддержке IDE (отсеивание вариантов, заведомо не подходящих по типу).

Преимущества динамической типизации

  • Лёгкость формирования многофункциональных коллекций – множество всякого (изредка появляется подобная потребность, но в то время когда появляется, динамическая типизация вам посодействует).
  • Комфортность изложения обобщенных алгоритмов (к примеру, распределение множества, которое помимо работы на списке целых чисел, будет также работать и на списке вещественных, и даже на списке строк).
  • Простота освоения – чаще всего языки с динамической типизацией хороши именно для начального этапа программирования.

Обобщенное программирование

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

Каким же образом мы решим такую задачу? С помощью 3-ех различных языков: один с динамической типизацией и два других со статической.

Алгоритм поиска будет один из элементарных – перебор. Функция получает искомый элемент, сам массив (или список) и впоследствии будет возвращать индекс элемента, или, если элемент не найден – (-1).

Динамическое решение (Python):

def find( required_element, list ):
    for (index, element) in enumerate(list):
        if element == required_element:
            return index

    return (-1)

Как вы можете видеть, все элементарно и никаких проблем с тем, что список может содержать как числа, списки, либо другие массивы нет. Отлично. А значит, следуем дальше и решим эту же задачу теперь на Си!

Статическое решение (Си):

unsigned int find_int( int required_element, int array[], unsigned int size ) {
    for (unsigned int i = 0; i < size; ++i )
        if (required_element == array[i])
            return i;

    return (-1);
}

unsigned int find_float( float required_element, float array[], unsigned int size ) {
    for (unsigned int i = 0; i < size; ++i )
        if (required_element == array[i])
            return i;

    return (-1);
}

unsigned int find_char( char required_element, char array[], unsigned int size ) {
    for (unsigned int i = 0; i < size; ++i )
        if (required_element == array[i])
            return i;

    return (-1);
}

Допустим, любая функция индивидуально сходна версии из Python, но отчего же их три? Неужто статическое программирование проиграло?

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

Статическое решение (обобщенное программирование, C++):

template <class T>
unsigned int find( T required_element, std::vector<T> array ) {
    for (unsigned int i = 0; i < array.size(); ++i )
        if (required_element == array[i])
            return i;

    return (-1);
}

Хорошо! Такое решение выглядит не намного сложнее, чем вариант на Python, а писать пришлось гораздо больше. Дополнительно мы получаем реализацию для всех массивов, а не только для 3-ех, которые необходимы для решения задачи!

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

Радует, что такое можно реализовать, но всегда есть возможность сделать ещё лучше. Во-первых, обобщенное программирование можно сделать гораздо удобнее и привлекательней как в языке Haskell. Во-вторых, кроме обобщенного программирования можно использовать полиморфизм (результат будет хуже), перегрузку функций (аналогично) или макросы.

Статика в динамике

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

C# поддерживает псевдо-тип dynamic.

F# поддерживает синтаксический сахар в виде оператора ?, на базе чего может быть реализована имитация динамической типизации.

Haskell — динамическая типизация обеспечивается модулем Data.Dynamic.

Delphi — посредством специального типа Variant.

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

Common Lisp — декларации типов.

Perl — с версии 5.6, довольно ограниченно.
Итак, идем дальше?

Сильная и слабая типизации

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

Слабо типизированные языки, напротив максимально содействуют для того, чтобы программист сочетал различные виды в одном выражении, более того компилятор сам приведет все к единому виду. Их называют «языки с нестрогой типизацией», с английского «weak typing».

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

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

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

Хотите узнать какие? Тогда продолжим.

Достоинства сильной типизации

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

Преимущества слабой типизации

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

Теперь, мы разобрались, что и у слабой типизации тоже есть преимущества! А есть ли методы переместить плюсы слабой типизации в сильную?

Оказывается, есть и даже два.

Неявное приведение типов, в конкретных ситуациях и без потерь данных.

Ух… Довольно длинный пункт. Предлагаю, дальше сокращать его до «ограниченное неявное преобразование». Так что же значит определённая ситуация и потери данных?

Определённая ситуация – это трансформация или операция, в которой сущность сразу понятна. К примеру, сложение двух чисел – определённая ситуация. А изменение числа в массив — нет (может, создастся массив из одного элемента, а возможно число трансформируется в строку, а после в массив символов).

Потеря данных это еще элементарней. Если мы изменим вещественное число 3.5 в целое, мы можем потерять часть данных (по сути, такая операция абсолютно неопределённая – как будет производиться округление? В большую сторону? В меньшую? Отбрасывать дробную часть?).

Трансформация в неопределённых ситуациях и трансформация с потерей данных – это крайне плохо. Ничего хуже этого в программировании нет.

Если вы мне не можете поверить в это то, изучите язык PL/I или просто поищите его спецификацию. В нем есть правила преобразования между ВСЕМИ типами данных!

Предлагаю вспомнить про ограниченное неявное изменение. Есть ли такие языки? Да, к примеру, в Pascal. Вы сможете изменить целое число в вещественное, но не противоположно. Кроме этого подобные механизмы есть в C#, Groovy и Common Lisp.

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

Я покажу его на примере прекрасного языка Haskell.

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

К примеру, в выражении pi + 1, не хочется писать pi + 1.0 или pi + float(1). Хочется написать просто pi + 1!

И это разработано в Haskell, по причине того, что у литерала 1 нет точного вида. Это ни единое, ни материальное, ни всестороннее. Это же просто число!

По итогу при написании простой функции sum x y, перемножающей все числа от x до y (с инкрементом в 1), мы обретаем некоторое количество версий: sum для единых, sum для материальных , sum для рациональных, sum для всесторонних чисел, и даже sum для всех тех числовых типов, что Вы сами установили.

Безусловно, такой приём поможет только в момент использования смешанных выражений с числовыми литералами, а это лишь начало.

Исходя из этого, можно сделать вывод о том, что наилучшим исходом будет соблюдение баланса, находясь на грани, между сильной и слабой типизацией. Но на данный момент совершенный баланс не соблюдает ни один язык, поэтому я склоняюсь к сильно типизированным языкам (таким как Haskell, Java, C#, Python), а не к слабо типизированным (таким как C, JavaScript, Lua, PHP).

Ладно, пойдем дальше?

Явная и неявная типизации

Язык с явной типизацией планирует, что программист будет отмечать типы всех переменных и функций, которые заявляет. С английского он означает «explicit typing».

Язык с неявной типизацией действует, наоборот, предлагая Вам не задумываться о типах и перевести подобную функцию на компилятор или интерпретатор. С английского он означает «implicit typing».

Изначально, кажется, что неявная типизация равна динамической, а явная – статической, но в дальнейшем мы поймём, что это неверно.

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

Преимущества явной типизации

Существование у той или иной функции сигнатуры (к примеру, int add(int, int)) даёт возможность без проблем выяснить, что осуществляет функция.

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

Преимущества неявной типизации

Уменьшение записи – def add (x, y) определённо короче, чем int add( int x, int y).

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

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

Явная типизация по выбору

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

-- Без явного указания типа
add (x, y) = x + y

-- Явное указание типа
add :: (Integer, Integer) -> Integer
add (x, y) = x + y

Примечание: я специально применил некаррированную функцию, а также намерено записал частную сигнатуру вместо общей add :: (Num a) => a -> a -> a*, так как хотел показать идею, без объяснения синтаксиса Haskell'а.

Как мы можем заметить, это крайне коротко и красиво выглядит. Вся функция занимает всего 18 символов на одной строчке, в том числе и пробелы!
Но автоматический вывод типов достаточно сложная вещь, и даже в таком крутом языке как Haskell, он периодически не справляется. (для наглядности можно показать ограничение мономорфизма)

Есть ли языки с явной типизацией по умолчанию и неявной по необходимости? Конечно.

Неявная типизация по-выбору

В новом нормативе языка C++, названном C++11 (ранее назывался C++0x), было введено ключевое слово auto, с помощью которого есть возможность принудить компилятор вывести тип, исходя из контекста:

Давайте сравним:
// Ручное указание типа
unsigned int a = 5;
unsigned int b = a + 3;

// Автоматический вывод типа
unsigned int a = 5;
auto b = a + 3;

Нормально. Однако запись сжалась минимально. Предлагаю рассмотреть пример с итераторами (если не понимаете, не беда, главное обратите внимание, как сократилась запись, благодаря автоматическому выводу):

// Ручное указание типа
std::vector<int> vec = randomVector( 30 );
for ( std::vector::const_iterator it = vec.cbegin(); ... ) { 
    ...
}

// Автоматический вывод типа
auto vec = randomVector<int>( 30 );
for ( auto it = vec.cbegin(); ... ) { 
    ...
}

Ого, вот это сокращение! Хорошо, но есть ли возможность создать что-то подобное в  Haskell, где тип возвращаемого значения будет зависеть от типов аргументов?

И в очередной раз я отвечу, да. При помощи  ключевого слова decltype в объединении с auto:

// Ручное указание типа
int divide( int x, int y ) {
    ...
}

// Автоматический вывод типа
auto divide( int x, int y ) -> decltype(x / y) {
    ...
}

Может возникнуть ощущение, что эта форма записи не достаточно хороша, но в комбинации с обобщенным программированием (templates / generics) неявная типизация или автоматический вывод типов творят невообразимое.

Некоторые языки программирования по данной классификации

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

JavaScript  - Динамическая | Слабая      | Неявная
Ruby        - Динамическая | Сильная     | Неявная
Python      - Динамическая | Сильная     | Неявная
Java        - Статическая  | Сильная     | Явная
PHP         - Динамическая | Слабая      | Неявная
C           - Статическая  | Слабая      | Явная
C++         - Статическая  | Слабая      | Явная
Perl        - Динамическая | Слабая      | Неявная
Objective-C - Статическая  | Слабая      | Явная
C#          - Статическая  | Сильная     | Явная
Haskell     - Статическая  | Сильная     | Неявная
Common Lisp - Динамическая | Сильная     | Неявная
D           - Статическая  | Сильная     | Явная
Delphi      - Статическая  | Сильная     | Явная

Примечания к таблице:

C# – сохраняет динамическую типизацию, с помощью особого псевдо-типа dynamic с версии 4.0. А также оказывает содействие неявной типизации посредством dynamic и var.

С++ – после стандарта C++11 обрёл опору неявной типизации посредством ключевых слов auto и decltype. Сохраняет динамическую типизацию, в момент использования библиотеки Boost (boost::any, boost::variant). Содержит в себе черты как сильной, так и слабой типизации.

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

D – также осуществляет поддержание неявной типизации.

Delphi – осуществляет поддержку динамической типизации, с помощью особого типа Variant.

Есть вероятность, что я где-то мог ошибиться, в особенности с CL, PHP и Obj-C, если по какому-либо языку у Вас есть другое мнение – пишите в комментариях.

Заключение

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

И удачи!

Полезные ссылки:

Прогопедия: типизации
Квадранты типизации в языках программирования

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ