Контакты

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

Типизация - назначения типа информационным сущностям.

Наиболее распространённые примитивные типы данных:

  • Числовой
  • Символьный
  • Логический

Основные функции системы типов данных:

  • Обеспечение безопасности
    Проверяется каждая операция на получение аргументов именно тех типов, для которых она имеет предназначена;
  • Оптимизация
    На основе типа выбирается способ эффективного хранения и алгоритмов его обработки;
  • Документация
    Подчеркивается намерения программиста;
  • Абстракция
    Использование типов данных высокого уровня позволяет программисту думать о значениях как о высокоуровневых сущностях, а не как о наборе битов.

Классификация

Есть множество классификаций типизаций языков программирования, но основные только 3:

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

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

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

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

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

Var luckyNumber = 777; var siteName = "Tyapk"; // подразумеваем число, записываем строку var wrongNumber = "999";

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

Let luckyNumber: number = 777; let siteName: string = "Tyapk"; // вызовет ошибку let wrongNumber: number = "999";

  • Статическая: Java, C#, TypeScript.
  • Динамическая: Python, Ruby, JavaScript.

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

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

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

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

Let stringVar = "777" + 99; // получим "77799"

Явная типизация (вымышленный язык похожий на JS)

Let wrongStringVar = "777" + 99; // вызовет ошибку let stringVar = "777" + String(99); // получим "77799"

Строгая / нестрогая типизация

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

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

Строгая типизация (вымышленный язык похожий на JS)

Let wrongNumber = 777; wrongNumber = wrongNumber + "99"; // получим ошибку, что к числовой переменной wrongNumber прибавляется строка let trueNumber = 777 + Number("99"); // получим 876

Нестрогая типизация (как есть в js)

Let wrongNumber = 777; wrongNumber = wrongNumber + "99"; // получили строку "77799"

  • Строгая: Java, Python, Haskell, Lisp.
  • Нестрогая: C, JavaScript, Visual Basic, PHP.

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

Не волнуйтесь, я знаю, что это всё звучит запутанно, поэтому мы начнём с основ. Что такое “проверка соответствия типов” и что такое вообще тип?

Тип

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

Типичные заблуждения

Миф 1: статическая / динамическая типизация == сильная / слабая типизация

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

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

X = 1 + "2"

Мы часто ассоциируем статически типизированные языки, такие как Java и C#, с сильно типизированным (они такими и являются), поскольку тип данных задаётся явно при инициализации переменной - как в этом примере на Java:

String foo = new String("hello world");

Тем не менее, Ruby, Python и JavaScript (все они обладaют динамической типизацией) также являются сильно типизированными, хотя разработчику и не нужно указывать тип переменной при объявлении. Рассмотрим такой же пример, но написанный на Ruby:

Foo = "hello world"

Оба языка являются сильно типизированными, но используют разные методы проверки типов. Такие языки, как Ruby, Python и JavaScript не требуют явного определения типов из-за вывода типов - способности программно выводить нужный тип переменной в зависимости от её значения. Вывод типов - это отдельное свойство языка, и не относится к системам типов.

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

$foo = "x"; $foo = $foo + 2; // not an error echo $foo; // 2

Поскольку PHP обладает слабой типизацией, ошибки в этом коде нет. Аналогично предыдущему предположению, не все слабо типизированные языки являются динамически типизированными: PHP - это динамически типизированный язык, но вот C - тоже язык со слабой типизацией - воистину статически типизирован.

Миф разрушен.

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

Миф 2: статическая / динамическая типизация == компилируемые / интерпретируемые языки

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

Когда мы говорим о типизации языка, мы говорим о языке как о целом. Например, неважно, какую версию Java вы используете - она всегда будет статически типизированной. Это отличается от того случая, когда язык является компилируемым или интерпретируемым, поскольку в этом случае мы говорим о конкретной реализации языка. В теории, любой язык может быть как компилируемым, так и интерпретируемым. Самая популярная реализация языка Java использует компиляцию в байткод, который интерпретирует JVM - но есть и иные реализации этого языка, которые компилируются напрямую в машинный код или интерпретируются как есть.

Если это всё ещё непонятно, советую прочесть этого цикла.

Заключение

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

Нет однозначного ответа на вопрос “какая типизация лучше?” - у каждой есть свои преимущества и недостатки. Некоторые языки - такие как Perl и C# - даже позволяют вам самостоятельно выбирать между статической и динамической системами проверки типов. Понимание этих систем позволит вам лучше понять природу возникающих ошибок, а также упростит борьбу с ними.

Чтобы максимально просто объяснить две абсолютно разные технологии, начнём сначала. Первое, с чем сталкивается программист при написании кода - объявление переменных. Вы можете заметить, что, например, в языке программирования C++ необходимо указывать тип переменной. То есть если вы объявляете переменную x, то обязательно нужно добавить int - для хранения целочисленных данных, float - для хранения данных с плавающей точкой, char - для символьных данных, и другие доступные типы. Следовательно, в C++ используется статическая типизация, так же как и в его предшественнике C.

Как работает статическая типизация?

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

Рассмотрим небольшой пример. При инициализации переменной x (int x;) мы указываем идентификатор int - это сокращение от который хранит только целые числа в диапазоне от - 2 147 483 648 до 2 147 483 647. Таким образом, компилятор понимает, что может выполнять над этой переменной математические значения - сумму, разность, умножение и деление. А вот, например, функцию strcat(), которая соединяет два значения типа char, применить к x нельзя. Ведь если снять ограничения и попробовать соединить два значения int символьным методом, тогда произойдет ошибка.

Зачем понадобились языки с динамической типизацией?

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

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

Какой вид типизации лучше?

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

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

Разделение на «сильную» и «слабую» типизацию

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

Особенность динамики

Наверняка вы замечали, что на стадии написании кода компилятор анализирует написанные конструкции и выдаст ошибку при несовпадении типов данных. Но только не JavaScript. Его уникальность в том, что он в любом случае произведет операцию. Вот легкий пример - мы хотим сложить символ и число, что не имеет смысла: «x» + 1.

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

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

Возможны ли смежные архитектуры?

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

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

  • В языке программирования Delphi - подсистема Variant.
  • В языке программирования AliceML - дополнительные пакеты.
  • В языке программирования Haskell - библиотека Data.Dynamic.

Когда строгая типизация действительно лучше динамической?

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

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

  • Сводит к минимуму количество символов и строк кода из-за ненадобности предварительного объявления переменных и указания их типа. Тип будет определен автоматически, после присвоения значения.
  • В небольших блоках кода упрощается визуальное и логическое восприятие конструкций, из-за отсутствия «лишних» строк объявления.
  • Динамика положительно влияет на скорость работы компилятора, так как он не учитывает типы, и не проверяет их на соответствие.
  • Повышает гибкость и позволяет создавать универсальные конструкции. К примеру, при создании метода, который должен взаимодействовать с массивом данных, не нужно создавать отдельные функции для работы с числовыми, текстовыми и другими типами массивов. Достаточно написать один метод, и он будет работать с любыми типами.
  • Упрощает вывод данных из систем управления базами данных, поэтому динамическую типизацию активно используют при разработке веб-приложений.

Подробнее о языках программирования со статической типизацией

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

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

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

Подробнее о языках программирования с динамическим видом типизации

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

  • PHP - язык для создания скриптов. Повсеместно применяется в веб-разработке, обеспечивая взаимодействие с базами данных, для создания интерактивных динамических веб-страниц. Благодаря динамической типизации существенно облегчается работы с базами данных.

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

Динамический вид типизации - недостатки

  • Если была допущена опечатка или грубая ошибка при использовании или объявлении переменных, то компилятор не отобразит её. А проблемы возникнут при выполнении программы.
  • При использовании статической типизации все объявления переменных и функций обычно выносятся в отдельный файл, который позволяет в дальнейшем с легкостью создать документацию или вообще использовать сам файл как документацию. Соответственно, динамическая типизация не позволяет использовать такую особенность.

Подведем итог

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

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



Тип - это коллекция возможных значений. Целое число может обладать значениями 0, 1, 2, 3 и так далее. Булево может быть истиной или ложью. Можно придумать свой тип, например, тип "ДайПять", в котором возможны значения "дай" и "5", и больше ничего. Это не строка и не число, это новый, отдельный тип.


Статически типизированные языки ограничивают типы переменных: язык программирования может знать, например, что x - это Integer. В этом случае программисту запрещается делать x = true , это будет некорректный код. Компилятор откажется компилировать его, так что мы не сможем даже запустить такой код. Другой статически типизированный язык может обладать другими выразительными возможностями, и никакая из популярных систем типов не способна выразить наш тип ДайПять (но многие могут выразить другие, более изощренные идеи).


Динамически типизированные языки помечают значения типами: язык знает, что 1 это integer, 2 это integer, но он не может знать, что переменная x всегда содержит integer.


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

Статически типизированные языки

Статические языки проверяют типы в программе во время компиляции, еще до запуска программы. Любая программа, в которой типы нарушают правила языка, считается некорректной. Например, большинство статических языков отклонит выражение "a" + 1 (язык Си - это исключение из этого правила). Компилятор знает, что "a" - это строка, а 1 - это целое число, и что + работает только когда левая и правая часть относятся к одному типу. Так что ему не нужно запускать программу чтобы понять, что существует проблема. Каждое выражение в статически типизированном языке относится к определенному типу, который можно определить без запуска кода.


Многие статически типизированные языки требуют обозначать тип. Функция в Java public int add(int x, int y) принимает два целых числа и возвращает третье целое число. Другие статически типизированные языки могут определить тип автоматически. Та же самая функция сложения в Haskell выглядит так: add x y = x + y . Мы не сообщаем языку типы, но он может определить их сам, потому что знает, что + работает только на числах, так что x и y должны быть числами, значит функция add принимает два числа как аргументы.


Это не уменьшает "статичность" системы типов. Система типов в Haskell знаменита своей статичностью, строгостью и мощностью, и в по всем этим фронтам Haskell опережает Java.

Динамически типизированные языки

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


def f(x, y): return x + y

может складывать два целых числа, склеивать строки, списки и так далее, и мы не можем понять, что именно происходит, пока не запустим программу. Возможно, в какой-то момент функцию f вызовут с двумя строками, и с двумя числами в другой момент. В таком случае x и y будут содержать значения разных типов в разное время. Поэтому говорят, что значения в динамических языках обладают типом, но переменные и функции - нет. Значение 1 это определенно integer, но x и y могут быть чем угодно.

Сравнение

Большинство динамических языков выдадут ошибку, если типы используются некорректно (JavaScript - известное исключение; он пытается вернуть значение для любого выражения, даже когда оно не имеет смысла). При использовании динамически типизированных языков даже простая ошибка вида "a" + 1 может возникнуть в боевом окружении. Статические языки предотвращают такие ошибки, но, конечно, степень предотвращения зависит от мощности системы типов.


Статические и динамические языки построены на фундаментально разных идеях о корректности программ. В динамическом языке "a" + 1 это корректная программа: код будет запущен и появится ошибка в среде исполнения. Однако, в большинстве статически типизированных языков выражение "a" + 1 - это не программа : она не будет скомпилирована и не будет запущена. Это некорректный код, так же, как набор случайных символов!&%^@*&%^@* - это некорректный код. Это дополнительное понятие о корректности и некорректности не имеет эквивалента в динамических языках.

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

Понятия "сильный" и "слабый" - очень неоднозначные. Вот некоторые примеры их использования:

    Иногда "сильный" означает "статический".
    Тут все просто, но лучше использовать термин "статический", потому что большинство используют и понимают его.

    Иногда "сильный" означает "не делает неявное преобразование типов".
    Например, JavaScript позволяет написать "a" + 1 , что можно назвать "слабой типизацией". Но почти все языки предоставляют тот или иной уровень неявного преобразования, которое позволяет автоматически переходить от целых чисел к числам с плавающей запятой вроде 1 + 1.1 . В реальности, большинство людей используют слово "сильный" для определения границы между приемлемым и неприемлемым преобразованием. Нет какой-то общепринятой границы, они все неточные и зависят от мнения конкретного человека.

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

  • Иногда "сильный" означает безопасный для памяти (memory-safe).
    Си - это пример небезопасного для памяти языка. Если xs - это массив четырех чисел, то Си с радостью выполнит код xs или xs , возвращая какое-то значение из памяти, которая находится сразу за xs .

Давайте остановимся. Вот как некоторые языки отвечают этим определениям. Как можно заметить, только Haskell последовательно "сильный" по всем параметрам. Большинство языков не такие четкие.



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


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


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



Сильная типизация: Система типов, которую я люблю и с которой мне комфортно.

Слабая типизация: Система типов, которая беспокоит меня или с которой мне не комфортно.

Постепенная типизация (gradual typing)

Можно ли добавить статические типы в динамические языки? В некоторых случаях - да. В других это сложно или невозможно. Самая очевидная проблема - это eval и другие похожие возможности динамических языков. Выполнение 1 + eval("2") в Python дает 3. Но что даст 1 + eval(read_from_the_network()) ? Это зависит от того, что в сети на момент выполнения. Если получим число, то выражение корректно. Если строку, то нет. Невозможно узнать до запуска, так что невозможно анализировать тип статически.


Неудовлетворительное решение на практике - это задать выражению eval() тип Any, что напоминает Object в некоторых объектно-ориентированных языках программирования или интерфейс interface {} в Go: это тип, которому удовлетворяет любое значение.


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


В некоторых языках есть опциональная или постепенная типизация (gradual typing): они динамические по умолчанию, но позволяют добавлять некоторые статические аннотации. В Python недавно добавили опциональные типы; TypeScript - это надстройка над JavaScript, в котором есть опциональные типы; Flow производит статический анализ старого доброго кода на JavaScript.


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

Компиляция статически типизированного кода

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


Компиляторы статических языков обычно могут генерировать более быстрый код, чем компиляторы динамических. Например, если компилятор знает, что функция add принимает целые числа, то он может использовать нативную инструкцию ADD центрального процессора. Динамический язык будет проверять тип при выполнении, выбирая один из множества функций add в зависимости от типов (складываем integers или floats или склеиваем строки или, может быть, списки?) Или нужно решить, что возникла ошибка и типы не соответствуют друг другу. Все эти проверки занимают время. В динамических языках используются разные трюки для оптимизации, например JIT-компиляция (just-in-time), где код перекомпилируется при выполнении после получения всей необходимой о типах информации. Однако, никакой динамический язык не может сравниться по скоростью с аккуратно написанным статическим кодом на языке вроде Rust.

Аргументы в пользу статических и динамических типов

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


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


Плюсы и минусы статических и динамических систем типизации все еще плохо изучены, но они определенно зависят от языка и конкретной решаемой задачи.


JavaScript пытается продолжить работу, даже если это означает бессмысленную конвертацию (вроде "a" + 1 , дающее "a1"). Python в свою очередь старается быть консервативным и часто возвращает ошибки, как в случае с "a" + 1 .


Существуют разные подходы с разными уровнями безопасности, но Python и JavaScript оба являются динамически типизированными языками.



Haskell же не позволит сложить integer и float без явного преобразования перед этим. Си и Haskell оба являются статически типизированными, не смотря на такие большие отличия.


Есть множество вариаций динамических и статических языков. Любое безоговорочное высказывание вида "статические языки лучше, чем динамические, когда дело касается Х" - это почти гарантированно ерунда. Это может быть правдой в случае конкретных языков, но тогда лучше сказать "Haskell лучше, чем Python когда дело касается Х".

Разнообразие статических систем типизации

Давайте взглянем на два знаменитых примера статически типизированных языков: Go и Haskell. В системе типизации Go нет обобщенных типов, типов с "параметрами" от других типов. Например, можно создать свой тип для списков MyList, который может хранить любые нужные нам данные. Мы хотим иметь возможность создавать MyList целых чисел, MyList строк и так далее, не меняя исходный код MyList. Компилятор должен следить за типизацией: если есть MyList целых чисел, и мы случайно добавляем туда строку, то компилятор должен отклонить программу.


Go специально был спроектирован таким образом, чтобы невозможно было задавать типы вроде MyList. Лучшее, что возможно сделать, это создать MyList "пустых интерфейсов": MyList может содержать объекты, но компилятор просто не знает их тип. Когда мы достаем объекты из MyList, нам нужно сообщить компилятору их тип. Если мы говорим "Я достаю строку", но в реальности значение - это число, то будет ошибка исполнения, как в случае с динамическими языками.


В Go также нет множества других возможностей, которые присутствуют в современных статически типизированных языках (или даже в некоторых системах 1970-х годов). У создателей Go были свои причины для этих решений, но мнение людей со стороны по этому поводу иногда может звучать резко.


Теперь давайте сравним с Haskell, который обладает очень мощной системой типов. Если задать тип MyList, то тип "списка чисел" это просто MyList Integer . Haskell не даст нам случайно добавить строку в список, и удостоверится, что мы не положим элемент из списка в строковую переменную.


Haskell может выражать намного более сложные идеи напрямую типами. Например, Num a => MyList a означает "MyList значений, которые относятся к одному типу чисел". Это может быть список integer"ов, float"ов или десятичных чисел с фиксированной точностью, но это определенно никогда не будет списком строк, что проверяется при компиляции.


Можно написать функцию add, которая работает с любыми численными типами. У этой функции будет тип Num a => (a -> a -> a) . Это означает:

  • a может быть любым численным типом (Num a =>).
  • Функция принимает два аргумента типа a и возвращает тип a (a -> a -> a).

Последний пример. Если тип функции это String -> String , то она принимает строку и возвращает строку. Но если это String -> IO String , то она также совершает какой-то ввод/вывод. Это может быть обращение к диску, к сети, чтение из терминала и так далее.


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


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


Сравните эту мощность с Go, который не способен выразить простую идею MyList, не говоря уже о "функции, которая принимает два аргумента, и они оба численные и одного типа, и которая делает ввод/вывод".


Подход Go упрощает написание инструментов для программирования на Go (в частности, реализация компилятора может быть простой). К тому же, требуется изучить меньше концепций. Как эти преимущества сравнимы со значительными ограничениями - субъективный вопрос. Однако, нельзя поспорить, что Haskell сложнее изучить, чем Go, и что система типов в Haskell намного мощнее, и что Haskell может предотвратить намного больше типов багов при компиляции.


Go и Haskell настолько разные языки, что их группировка в один класс "статических языков" может вводить в заблуждение, не смотря на то, что термин используется корректно. Если сравнивать практические преимущества безопасности, то Go ближе к динамических языкам, нежели к Haskell"у.


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

Конкретные примеры отличия в возможностях систем типизации

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


В Go можно сказать "функция add принимает два integer"а и возвращает integer":


func add(x int, y int) int { return x + y }

В Haskell можно сказать "функция принимает любой численный тип и возвращает число того же типа":


f:: Num a => a -> a -> a add x y = x + y

В Idris можно сказать "функция принимает два integer"а и возвращает integer, но первый аргумент должен быть меньше второго аргумента":


add: (x: Nat) -> (y: Nat) -> {auto smaller: LT x y} -> Nat add x y = x + y

Если попытаться вызвать функцию add 2 1 , где первый аргумент больше второго, то компилятор отклонит программу во время компиляции . Невозможно написать программу, где первый аргумент больше второго. Редкий язык обладает такой возможностью. В большинстве языков такая проверка происходит при выполнении: мы бы написали что-то вроде if x >= y: raise SomeError() .


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

Системы типизации некоторых статических языков

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

  • C (1972), Go (2009) : Эти системы совсем не мощные, без поддержки обобщенных типов. Невозможно задать тип MyList, который бы означал "список целых чисел", "список строк" и т.д. Вместо этого придется делать "список необозначенных значений". Программист должен вручную сообщать "это список строк" каждый раз, когда строка извлекается из списка, и это может привести к ошибке при исполнении.
  • Java (1995), C# (2000) : Оба языка поддерживают обобщенные типы, так что можно сказать MyList и получить список строк, о котором компилятор знает и может следить за соблюдением правил типов. Элементы из списка будут обладать типом String, компилятор будет форсировать правила при компиляции как обычно, так что ошибки при исполнении менее вероятны.
  • Haskell (1990), Rust (2010), Swift (2014) : Все эти языки обладают несколькими продвинутыми возможностями, в том числе обобщенными типами, алгебраическими типами данных (ADTs), и классами типов или чем-то похожим (типы классов, признаки (traits) и протоколы, соответственно). Rust и Swift более популярны, чем Haskell, и их продвигают крупные организации (Mozilla и Apple, соответственно).
  • Agda (2007), Idris (2011) : Эти языки поддерживают зависимые типы, позволяя создавать типы вроде "функция, которая принимает два целых числа х и y, где y больше, чем x". Даже ограничение "y больше, чем x" форсируется при компиляции. При выполнении y никогда не будет меньше или равно x, что бы ни случилось. Очень тонкие, но важные свойства системы могут быть проверены статически в этих языках. Их изучает очень мало программистов, но эти языки вызывают у них огромный энтузиазм.

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


Группа два (Java и C#) - это мэйнстримовые языки, зрелые и широко используемые.


Группа три находится на пороге входа в мэйнстрим, с большой поддержкой со стороны Mozilla (Rust) и Apple (Swift).


Группа четыре (Idris and Agda) далеки от мэйнстрима, но это может измениться со временем. Языки группы три были далеко от мэйнстрима еще десять лет назад.

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

Строгая типизация подразумевает выполнение следующих обязательных условий:

  1. Любой объект данных (переменная, константа, выражение) в языке всегда имеет строго определённый тип , который фиксируется на момент компиляции программы (статическая типизация) или определяется во время выполнения (динамическая типизация).
  2. Допускается присваивание переменной только значения, имеющего строго тот же тип данных, что и переменная, те же ограничения действуют в отношении передачи параметров и возврата результатов функций.
  3. Каждая операция требует параметров строго определённых типов.
  4. Неявное преобразование типов не допускается (то есть транслятор воспринимает любую попытку использовать значение не того типа, который был описан для переменной, параметра, функции или операции, как синтаксическую ошибку).

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

Единственный практически используемый язык программирования со строгой типизацией - это Ада . Довольно большое число распространённых языков программирования используют нестрогую статическую типизацию. К таким языкам относятся, например Pascal , Модула-2 , Java . В них обязательно описание типов переменных, параметров и функций, но допускается неявное приведение типов - в случае, если значение одного типа присваивается переменной другого, то компилятор автоматически генерирует код для преобразования значения в нужный тип, если только такое преобразование не приводит к потере данных. Так, например, целое число можно присваивать переменной, объявленной как число с плавающей точкой, а обратное присваиваение без явного приведения типов запрещено, поскольку с высокой вероятностью приведёт к ошибке. Некоторые языки, формально имеющие понятие типа данных, в действительности можно считать нетипизированными. К таким языкам относится классический Си , в котором, хотя объявление типов и требуется, в действительности все типы данных являются совместимыми по присваиванию (современные компиляторы Си ограничивают эту свободу и выдают, по меньшей мере, предупреждения при опасных преобразованиях типов).

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

Понравилась статья? Поделитесь ей