Динамическая типизация лучше чем строгая. Любой язык - Зачем нужна динамическая типизация? Подробнее о языках программирования с динамическим видом типизации

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

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

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

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

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

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

Есть множество классификаций типизаций языков программирования, но основные только 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.

Хотя возможны и промежуточные варианты, здесь представлены два главных подхода:

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

Эти термины легко объяснимы: при динамической типизации проверка типов происходит во время работы системы (динамически), а при статической типизации проверка выполняется над текстом статически (до выполнения).

Статическая типизация предполагает автоматическую проверку, возлагаемую, как правило, на компилятор. В итоге имеем простое определение:

Определение: статически типизированный язык

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

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

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

Правила типизации

Наша ОО-нотация является статически типизированной. Ее правила типов были введены в предыдущих лекциях и сводятся к трем простым требованиям.

  • При объявлении каждой сущности или функции должен задаваться ее тип, например, acc : ACCOUNT . Каждая подпрограмма имеет 0 или более формальных аргументов, тип которых должен быть задан, например: put (x: G; i: INTEGER) .
  • В любом присваивании x:= y и при любом вызове подпрограммы, в котором y - это фактический аргумент для формального аргумента x , тип источника y должен быть совместим с типом цели x . Определение совместимости основано на наследовании: B совместим с A , если является его потомком, - дополненное правилами для родовых параметров (см. "Введение в наследование").
  • Вызов x.f (arg) требует, чтобы f был компонентом базового класса для типа цели x , и f должен быть экспортирован классу, в котором появляется вызов (см. 14.3).

Реализм

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

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

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

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

Будем говорить, что язык реалистичен , если он пригоден к применению и полезен на практике. В отличие от определения статической типизации , дающего безапелляционный ответ на вопрос: " Типизирован ли язык X статически ?", определение реализма отчасти субъективно.

В этой лекции мы убедимся, что предлагаемая нами нотация реалистична.

Пессимизм

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

Рассмотрим обычный, необъектный, Pascal-подобный язык с различными типами REAL и INTEGER . При описании n: INTEGER; r: Real оператор n:= r будет отклонен, как нарушающий правила. Так, компилятор отвергнет все нижеследующие операторы:

n:= 0.0 [A] n:= 1.0 [B] n:= -3.67 [C] n:= 3.67 - 3.67 [D]

Если мы разрешим их выполнение, то увидим, что [A] будет работать всегда, так как любая система счисления имеет точное представление вещественного числа 0,0, недвусмысленно переводимое в 0 целых. [B] почти наверняка также будет работать. Результат действия [C] не очевиден (хотим ли мы получить итог округлением или отбрасыванием дробной части?). [D] справится со своей задачей, как и оператор:

if n ^ 2 < 0 then n:= 3.67 end [E]

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

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

Вопрос не в том, будем ли мы пессимистами, а в том, насколько пессимистичными мы можем позволить себе быть. Вернемся к требованию реализма : если правила типов настолько пессимистичны, что препятствуют простоте записи вычислений, мы их отвергнем. Но если достижение безопасности типов достигается небольшой потерей выразительной силы, мы примем их. Например, в среде разработки, предоставляющей функции округления и выделения целой части - round и truncate , оператор n:= r считается некорректным справедливо, поскольку заставляет вас явно записать преобразование вещественного числа в целое, вместо использования двусмысленных преобразований по умолчанию.

Статическая типизация: как и почему

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

Преимущества

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

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

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


Рис. 17.1.

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

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

Ключом к статической типизации является уже высказанная идея о том, что компилятор, генерирующий код для конструкции x.f (arg) , знает тип x . Из-за полиморфизма нет возможности однозначно определить подходящую версию компонента f . Но объявление сужает множество возможных типов, позволяя компилятору построить таблицу, обеспечивающую доступ к правильному f с минимальными издержками, - с ограниченной константой сложностью доступа. Дополнительно выполняемые оптимизации статического связывания (static binding) и подстановки (inlining) - также облегчаются благодаря статической типизации , полностью устраняя издержки в тех случаях, когда они применимы.

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

Несмотря на все это, динамическая типизация не теряет своих приверженцев, в частности, среди Smalltalk-программистов. Их аргументы основаны прежде всего на реализме, речь о котором шла выше. Они уверены, что статическая типизация чересчур ограничивает их, не давая им свободно выражать свои творческие идеи, называя иногда ее "поясом целомудрия".

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

Типизация: слагаемые успеха

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

Наша система типов полностью основана на понятии класса . Классами являются даже такие базовые типы, как INTEGER , а стало быть, нам не нужны особые правила описания предопределенных типов. (В этом наша нотация отличается от "гибридных" языков наподобие Object Pascal , Java и C++, где система типов старых языков сочетается с объектной технологией, основанной на классах.)

Развернутые типы дают нам больше гибкости, допуская типы, чьи значения обозначают объекты, наряду с типами, чьи значения обозначают ссылки.

Решающее слово в создании гибкой системы типов принадлежит наследованию и связанному с ним понятию совместимости . Тем самым преодолевается главное ограничение классических типизированных языков, к примеру, Pascal и Ada, в которых оператор x:= y требует, чтобы тип x и y был одинаковым. Это правило слишком строго: оно запрещает использовать сущности, которые могут обозначать объекты взаимосвязанных типов (SAVINGS_ACCOUNT и CHECKING_ACCOUNT ). При наследовании мы требуем лишь совместимости типа y с типом x , например, x имеет тип ACCOUNT , y - SAVINGS_ACCOUNT , и второй класс - наследник первого.

На практике статически типизированный язык нуждается в поддержке множественного наследования . Известны принципиальные обвинения статической типизации в том, что она не дает возможность по-разному интерпретировать объекты. Так, объект DOCUMENT (документ) может передаваться по сети, а потому нуждается в наличия компонентов, связанных с типом MESSAGE (сообщение). Но эта критика верна только для языков, ограниченных единичным наследованием .


Рис. 17.2.

Универсальность необходима, например, для описания гибких, но безопасных контейнерных структур данных (например class LIST [G] .. .). Не будь этого механизма, статическая типизация потребовала бы объявления разных классов для списков, отличающихся типом элементов.

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

class SORTABLE_LIST ...

Любой фактический родовой параметр SORTABLE_LIST должен быть потомком класса COMPARABLE , имеющего необходимый компонент.

Еще один обязательный механизм - попытка присваивания - организует доступ к тем объектам, типом которых ПО не управляет. Если y - это объект базы данных или объект, полученный через сеть, то оператор x ?= y присвоит x значение y , если y имеет совместимый тип, или, если это не так, даст x значение Void .

Утверждения , связанные, как часть идеи Проектирования по Контракту, с классами и их компонентами в форме предусловий, постусловий и инвариантов класса, дают возможность описывать семантические ограничения, которые не охватываются спецификацией типа . В таких языках, как Pascal и Ada, есть типы-диапазоны, способные ограничить значения сущности, к примеру, интервалом от 10 до 20, однако, применяя их, вам не удастся добиться того, чтобы значение i являлось отрицательным, всегда вдвое превышая j . На помощь приходят инварианты классов, призванные точно отражать вводимые ограничения, какими бы сложными они не были.

Закрепленные объявления нужны для того, чтобы на практике избегать лавинного дублирования кода. Объявляя y: like x , вы получаете гарантию того, что y будет меняться вслед за любыми повторными объявлениями типа x у потомка. В отсутствие этого механизма разработчики беспрестанно занимались бы повторными объявлениями, стремясь сохранить соответствие различных типов.

Закрепленные объявления - это особый случай последнего требуемого нам языкового механизма - ковариантности , подробное обсуждение которого нам предстоит позже.

При разработке программных систем на деле необходимо еще одно свойство, присущее самой среде разработки - быстрая, возрастающая (fast incremental) перекомпиляция . Когда вы пишите или модифицируете систему, хотелось бы как можно скорее увидеть эффект изменений. При статической типизации вы должны дать компилятору время на перепроверку типов. Традиционные подпрограммы компиляции требуют повторной трансляции всей системы (и ее сборки ), и этот процесс может быть мучительно долгим, особенно с переходом к системам большого масштаба. Это явление стало аргументом в пользу интерпретирующих систем, таких как ранние среды Lisp или Smalltalk, запускавшие систему практически без обработки, не выполняя проверку типов. Сейчас этот аргумент позабыт. Хороший современный компилятор определяет, как изменился код с момента последней компиляции, и обрабатывает лишь найденные изменения.

"Типизирована ли кроха"?

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

Самой распространенной лазейкой в статически типизированных языках является наличие преобразований, меняющих тип сущности . В C и производных от него языках их называют "приведением типа" или кастингом ( cast ). Запись (OTHER_TYPE) x указывает на то, что значение x воспринимается компилятором, как имеющее тип OTHER_TYPE , при соблюдении некоторых ограничений на возможные типы.

Подобные механизмы обходят ограничения проверки типов. Приведение широко распространено при программировании на языке C, включая диалект ANSI C. Даже в языке C++ приведение типов, хотя и не столь частое, остается привычным и, возможно, необходимым делом.

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

Типизация и связывание

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

Как типизация, так и связывание имеют дело с семантикой Базисной Конструкции x.f (arg) , но отвечают на два разных вопроса:

Типизация и связывание

  • Вопрос о типизации : когда мы должны точно знать, что во время выполнения появится операция, соответствующая f , применимая к объекту, присоединенному к сущности x (с параметром arg )?
  • Вопрос о связывании : когда мы должны знать, какую операцию инициирует данный вызов?

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

В рамках объектного подхода:

  • проблема, возникающая при типизации, связана с полиморфизмом : поскольку x во время выполнения может обозначать объекты нескольких различных типов, мы должны быть уверены, что операция, представляющая f , доступна в каждом из этих случаев;
  • проблема связывания вызвана повторными объявлениями : так как класс может менять наследуемые компоненты, то могут найтись две или более операции, претендующие на то, чтобы представлять f в данном вызове.

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

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

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

Причина выбора статической типизации и динамического связывания очевидна. Первый вопрос: "Когда мы будем знать о существовании компонентов?" - предполагает статический ответ: " Чем раньше, тем лучше ", что означает: во время компиляции. Второй вопрос: "Какой из компонентов использовать?" предполагает динамический ответ: " тот, который нужен ", - соответствующий динамическому типу объекта, определяемому во время выполнения. Это единственно приемлемое решение, если статическое и динамическое связывание дает различные результаты.

При статической типизации компилятор не отклонит вызов, если можно гарантировать, что при выполнении программы к сущности my_aircraft будет присоединен объект, поставляемый с компонентом, соответствующим lower_landing_gear . Базисная техника получения гарантий проста: при обязательном объявлении my_aircraft требуется, чтобы базовый класс его типа включал такой компонент. Поэтому my_aircraft не может быть объявлен как AIRCRAFT , так как последний не имеет lower_landing_gear на этом уровне; вертолеты, по крайней мере в нашем примере, выпускать шасси не умеют. Если же мы объявим сущность как PLANE , - класс, содержащий требуемый компонент, - все будет в порядке.

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

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

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

Рекомендую прочитать сначала краткую версию статьи, а затем при наличии желания и полную.

Краткая версия

Языки программирования по типизации принято делить на два больших лагеря - типизированные и нетипизированные (бестиповые). К первому например относятся 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) то, что все проверки типов выполняются на этапе компиляции, а не этапе выполнения.

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

Давайте разберемся.

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

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

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

Как же мы будем ее решать? Решим ее на 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 unsigned int find(T required_element, std::vector 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 vec = randomVector(30); for (std::vector::const_iterator it = vec.cbegin(); ...) { ... } // Автоматический вывод типа auto vec = randomVector(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 - Динамическая / Сильная / Неявная

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

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

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

Рекомендую прочитать сначала краткую версию статьи, а затем при наличии желания и полную.

Краткая версия

Языки программирования по типизации принято делить на два больших лагеря - типизированные и нетипизированные (бестиповые). К первому например относятся 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) то, что все проверки типов выполняются на этапе компиляции, а не этапе выполнения.

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

Давайте разберемся.

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

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

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

Как же мы будем ее решать? Решим ее на 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 unsigned int find(T required_element, std::vector 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 vec = randomVector(30); for (std::vector::const_iterator it = vec.cbegin(); ...) { ... } // Автоматический вывод типа auto vec = randomVector(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 - Динамическая / Сильная / Неявная

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

Заключение

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

Дело в том, что отсутствие типов убивает сразу много зайцев. Главные - это читаемость кода, и повышение простоты программирования в целом. Первое - это очень важно стратегически, чтобы при большом объёме кода, долгой разработке проект по прежнему был читаем и расширяем, второе - тактически - чтобы быстро решать задачи. Но приверженец строгой типизации может не почувствовать этих плюсов на первых порах, например я сам перешёл когда-то с C++ на JS и PHP - и наверное ещё год негодовал. Постараюсь объяснить..

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

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

Читаемость . Если грамотно именовать, переменные в коде JS - то код будет читаться как книга, несмотря на структурную сложность. Но когда к переменным добавляются типы - читаемость сильно теряет: читающий программист получает много лишней информации. Вот в быту мы говорим Убери яблоко в холодильник а не Убери красное круглое яблоко в большой серебристый холодильник, так как первое проще воспринимается. Например, чтение JS кода любой широко-распостраннённой библиотеки я бы сравнил с чтением стихов Пушкина, тогда как чтение типизированного C++ кода с чтением Иллиады Гомера (теперь уже).

Про простоту программирования без типов . Выходит так, что без типов, и с развитыми функциональными инструментами: половина паттернов не нужна, вторая половина паттернов записывается в три строчки вместо 15-ти. Классическая реализация старых паттернов GoF в типизированных языках сегодня больше похожа на костыли, которые просто есть, чтобы компенсировать недостаток гибкости при наличии строгой типизации - соответственно они нечитаемы, с ними надо серьёзнее разбираться, забивать себе голову вопросами "зачем это?" "это фасад или адаптер?", и.т.п. В JS же появляются плюс к старым совсем другие подходы, основанные на замыканиях, инкапсуляции, и ФП. При реализации одной-и-той же задачи в стиле ООП-классики на TS, и в стиле ФП на JS, что выходит в итоге:

  1. Код на JS получается компактнее раза в 3, и читаемее, чем код со строгой типизацией на том-же TS.
  2. Программист реализует задачу быстрее, пропорционально объёму кода.

Плюс JS работает в основном с древовидной базой данных под названием DOM - и его задача, это в основном управление этим DOM в динамике. То есть никаких серьёзных ООП структур не требуется, встроенного хватает с лихвой: обработки DOM событий, асинхронных возможностей JS. Это вкупе с отсутсвием типизации делает JS-код "квинтэссенцией управляющей логики". "Простота программирования" - это не значит, что у программистов JS будет халява, это значит что хороший программист успеет сделать в 3 раза больше.

Что теряет JS без типов?
Первое, что теряет JS - автоматическую валидацию входящих параметров функции - это почти главная роль которую исполняют типы в современных скриптовых языках. Параметры можно валидировать вручную в JS, но это делают крайне редко, разве что часто бывают проверки на пустоту вида if (!param1) return false; . Почему не делают? Да потому что JS проносит в себе стиль кода, в котором тип - это не главная характиристика переменной - и программисты быстро это понимают.
Второе что теряет JS - отсутствие возможности строить классическую ООП архитектуру. Нет, костылями и велосипедами конечно можно делать сильное подобие ООП - когда-то (далеко до ES6) этим вопросом задались создатели mootools, и сегодня 90% программистов JS, сталкивающихся с mootools считают его худшим фреймворком всех времён и народов) Почему? Да потому что в JS богатейший функционал, в том числе касаемо инкапсуляции(так необходимой для крупных проектов), множество способов делать "безопасные" для программы точки расширения, множество "своих паттернов", ООП ему не не нужен, чтобы сделать качественную расширяемую программу любого масштаба. Типизация, тем более строгая - есть локомотив "дедовского ООП" - и соответственно угнетатель гибких способов построения программ.
Последнее - это при работе в IDE автокомплит и "переход к классу" работают плохо. Это бывает, но если вы всё-таки решили писать ООП-подобный код в js, то воспользуйтесь JSDoc , чтобы помочь IDE. В то-же время плохая навигация, это проблема самого IDE, например PhpStorm(WebStorm) - работает с навигацией в JS хорошо и без JSDoc, и обещает, что скоро станет отлично.

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

UPD но дабы не превращать ответ в злостный холивар, надо заметить - что типы тоже полезны для той-же читаемости кода, например в аргументах функций. Но когда типы используются иногда, а не в 100% кода. Но если разрешить в JS пусть только типы в аргументах (например как в TS), программисты тут-же начнут злоупотреблять ООП, и убьют сформировавшуюся стилистику языка, и вместе с этим все плюсы JS - думаю это причина идеологии типизации EcmaScript. Бесполезна и даже вредна именно строгая типизация, но только если речь не идёт о максимальной производительности: тогда нет вопросов, строгая типизация выйграет.

nber-horeca.ru - Браузеры. Компьютер. Социальные сети. Программы