Академия с++. побег из темницы типов
Содержание:
Специальные системы типов
Ряд специальных систем типов был разработан для использования в определённых условиях с определёнными данными, а также для статического анализа программ. В большинстве своём они основываются на идеях формальной теории типов и используются лишь в составе исследовательских систем.
Экзистенциальные типы
Экзистенциальные типы, то есть типы, определённые посредством экзистенциального квантификатора (квантора существования), представляют собой механизм инкапсуляции на уровне типов: это композитный тип, скрывающий реализацию некоего типа в своём составе.
Понятие экзистенциального типа часто используется совместно с понятием типа записи для представления модулей и абстрактных типов данных, что обусловлено их назначением — отделением реализации от интерфейса. Например, тип описывает интерфейс модуля (семейства модулей с одинаковой сигнатурой), имеющий в своём составе значение данных типа и функцию, принимающую параметр в точности этого же типа и возвращающую целое число. Реализация может быть различной:
Оба типа являются подтипами более общего экзистенциального типа и соответствуют конкретно реализованным типам, так что любое значение, принадлежащее любому из них, принадлежит также к типу . Если — значение типа , то проходит проверку типов, вне зависимости от того, к какому абстрактному типу принадлежит . Это даёт гибкость при выборе типов, подходящих для конкретной реализации, так как пользователи извне обращаются только к значениям интерфейсного (экзистенциального) типа и изолированы от этих вариаций.
В общем случае механизм проверки согласования типов не способен определить, к какому именно экзистенциальному типу принадлежит данный модуль. В примере выше также мог бы иметь тип . Простейшим решением является явное указание для каждого модуля подразумеваемого в нём типа, например:
intT = { a: int; f: (int → int); } as ∃X { a: X; f: (X → int); }
Хотя абстрактные типы данных и модули использовались в языках программирования довольно давно, формальная модель экзистенциальных типов была построена лишь к 1988 году. Теория представляет собой типизированное лямбда-исчисление второго порядка, аналогичное Системе F, но с экзистенциальной квантификацией вместо универсальной.
Экзистенциальные типы явным образом доступны в качестве экспериментального расширения языка Haskell, где они представляют собой специальный синтаксис, позволяющий использовать переменную типа в определении алгебраического типа, не вынося её в сигнатуру конструктора типов, то есть не повышая его арность. Язык Java предоставляет ограниченную форму экзистенциальных типов посредством . В языках, реализующих классический в стиле ML, экзистенциальные типы могут быть симулированы посредством так называемых «значений, индексированных типами».
Проверка согласования типов
Процесс проверки и установления ограничений для типов — контроль типов или проверка соответствия типов — может осуществляться как (статическая типизация), так и во время выполнения (динамическая типизация). Если спецификация языка требует, чтобы правила типизации исполнялись строго (то есть допуская в той или иной мере лишь те автоматические преобразования типов, которые не теряют информацию), такой язык называется сильно типизированным (англ. strongly typed; в русской литературе преобладает вариант перевода строго типизированным), в противном случае — слабо типизированным. Эти термины являются условными и не используются в формальных обоснованиях.
Типизированные и бестиповые языки
Язык называют типизированным, если спецификация каждой операции определяет типы данных, к которым эта операция может применяться, подразумевая её неприменимость к иным типам. Например, данные, которые представляет «этот текст в кавычках», имеют тип «». В большинстве языков программирования деление числа на строку не имеет смысла, и большинство современных языков отвергнет программу, которая пытается выполнить такую операцию. В одних языках бессмысленная операция будет выявлена (статическая типизация), и отвергнута компилятором. В других она будет выявлена во время выполнения программы (динамическая типизация), порождая исключительную ситуацию.
Особый случай типизированных языков представляют однотиповые языки (англ. single-type language), то есть языки с единственным типом. Обычно это языки сценариев или разметки, такие как REXX и SGML, единственным типом данных в которых является символьная строка, используемая для представления как символьных, так и числовых данных.
Бестиповые языки, в противоположность типизированным, позволяют осуществлять любую операцию над любыми данными, которые в них представляются цепочками бит произвольной длины. Бестиповыми является большинство языков ассемблера. Примерами высокоуровневых бестиповых языков служат BCPL, , Forth, Рефал.
На практике, лишь некоторые языки могут считаться типизированными с точки зрения теории типов (разрешая или отвергая все операции), большинство современных языков предлагают лишь некую степень типизированности. Многие промышленные языки предоставляют возможность обойти или нарушить систему типов, поступаясь типобезопасностью ради более точного контроля над исполнением программы (каламбур типизации).
Особенность динамики
Наверняка вы замечали, что на стадии написании кода компилятор анализирует написанные конструкции и выдаст ошибку при несовпадении типов данных. Но только не JavaScript. Его уникальность в том, что он в любом случае произведет операцию. Вот легкий пример – мы хотим сложить символ и число, что не имеет смысла: «x» + 1.
В статических языках, в зависимости от самого языка, эта операция может иметь разные последствия. Но в большинстве случаев, её даже не допустят до компиляции, так как компилятор выдаст ошибку сразу после написания такой конструкции. Он просто посчитает её некорректной и будет полностью прав.
В динамических языках эту операцию выполнить можно, но в большинстве случаев последует ошибка уже на стадии выполнения кода, так как компилятор не анализирует в реальном времени типы данных и не может принимать решение об ошибках в этой области. JavaScript уникален тем, что выполнит такую операцию и получит набор нечитаемых символов. В отличие от других языков, которые просто завершат работу программы.
Перегрузка функций¶
TypeScript поддерживает возможность перегрузки функций, то есть мы можем определить несколько версий функции, которые будут иметь одно и то же имя, но разные типы параметров или разное количество параметров или разные возвращаемые типы результатов. Для перегрузки вначале опеределяем все версии функции, которые не будут иметь никакой логики. А потом определяем версию функции с общей сигнатурой, которая подходит под все ранее определенные варианты. И в этой общей версии уже определяем конкретную логику функции.
Например, нам надо объединить два значения, но если они представляют строки, то просто их конкатенировать, а если числа — то сложить. Тогда мы могли бы использовать следующую функцию:
Первая версия функции принимает две строки и возвращает строку, вторая версия принимает два числа и возвращает число. Общей для них будет функция, которая принимает параметры типа any и возвращает результат также типа .
Но если бы мы ту же функцию применили бы к логическим значениям:
то мы получили бы ошибку, так как две версии функции позволяют принимать в качестве параметров либо две строки, либо два числа. И в этом случае нам надо было бы добавить еще одну версию функции для логических значений:
Примеры
PHP
$res = "string1"; echo $res."\n"; // выводит "string1" - переменная имеет строковый тип. $res = 1; echo $res."\n"; // выводит "1" - переменная преобразуется в целочисленный тип. $res += 2; echo $res."\n"; // выводит "3" - результат операции: целочисленная переменная. $res .= "string2"; echo $res."\n"; // выводит "3string2" - переменная преобразуется в строковый тип и выполняется конкатенация.
Python
var = "string1" print(var) # Выведет "string1" var = 1 print(var) # Выведет "1" var += 2 print(var) # Выведет "3" # Однако, такой ход в Python невозможен: var += "string2" # Порождается исключение TypeError: unsupported operand type(s) for +=: 'int' and 'str' var = str(var) # и потребуется явное преобразование типов var += "string2" # теперь ОК print(var) # Выведет "3string2"
JavaScript
var res = "string1"; alert(res); // выводит 'string1' res = 1; alert(res); // выводит 1 res += 2; alert(res); // выводит 3 res += 'string2'; alert(res); // выводит '3string2'
Object Pascal
program Project2;
{$APPTYPE CONSOLE}
Uses SysUtils;
Var V1, V2 Variant;
begin
V1 := 'string1'; WriteLn(V1); // выводит "string1"
V2 := 1; WriteLn(V2); // выводит "1"
Inc(V2,2); WriteLn(V2); // выводит "3"
WriteLn(V2,'string2'); // выводит "3string2"
end.
Object Pascal: Другие способы использования динамической типизации
procedure TForm1.Myproc(Obj TObject);
begin
If (Obj is TButton)
then (Obj as TButton).Click;
end;
function Something (A array of const)
begin
// ...
end;
{Вызов: } Something ( 5,'Hello',3.14159, True, TForm );
procedure TForm1.DisplayValue(const AValue TValue);
begin
Memo1.Lines.Append(AValue.ToString);
end;
procedure TForm1.btn1Click(Sender TObject);
var
list TStrings;
begin
list := TStringList.Create();
list.Text := 'Foo';
try
DisplayValue(list);
DisplayValue(list.Count);
DisplayValue(list.Capacity * 8.964);
DisplayValue(list is TStringList);
DisplayValue(list.Text);
finally
list.Free;
end;
end;
{Вывод:
(TStringList @ 0166E460)
1
35,856
True
Foo
}
Lua
var="string" -- Переменная строкового типа
var={} -- Переменная стала таблицей
oldprint=print
print=1 -- Такое тоже возможно!
print("somestring") -- attempt to call a nil value
oldprint("somestring") -- somestring
Контекстный тип (Contextual Type)¶
Контекстным называется тип, который при не явном объявлении указывается за счет декларации контекста, а не с помощью вывода типов.
Лучшим примером контекстного типа может служить подписка на событие мыши . Так как у слушателя события, тип параметра не указан явно, а также ему в момент объявления не было присвоено значение, то, как мы уже знаем, вывод типов должен был указать тип . Но в данном случае компилятор указывает тип , потому что именно он указан в декларации типа слушателя событий. В случае подписания на событие , компилятор указывает тип как .
Для того чтобы понять, как это работает, опишем случай из жизни зоопарка, а именно — представление с морским львом. Для этого создадим класс морской лев и объявим в нем два метода: вращаться () и голос ().
Далее, создадим класс дрессировщик и объявим в нем метод с двумя параметрами: с типом и с типом .
Затем объявим два класса события, выражающие команды дрессировщика и .
После объявим два псевдонима () для литеральных типов . Первому зададим имя и в качестве значения присвоим строковой литерал . Второму зададим имя и в качестве значения присвоим строковой литерал .
Теперь осталось только задекларировать ещё два псевдонима типов для функциональных типов, у обоих из которых будет один параметр и отсутствовать возвращаемое значение. Первому псевдониму зададим имя , а его параметру установим тип . Второму псевдониму зададим имя , а его параметру установим тип .
Соберём части воедино. Для этого в классе дрессировщик перегрузим метод . У первого перегруженного метода параметр будет иметь тип , а параметру укажем тип . Второму перегруженному методу в качестве типа параметра укажем , а параметру укажем тип .
Осталось только убедиться что все работает правильно. Для этого создадим экземпляр класса и подпишемся на события. Сразу же можно увидеть подтверждение того, что цель достигнута. У слушателя события параметру указан контекстный тип . А слушателю события параметру указан контекстный тип .
Применение динамической типизации
Итак, допустим, у нас есть протокол удаленного вызова функций, как вариант, это параметризация SQL-запроса к базе данных. Типы аргументов и результата мы вычисляем на этапе выполнения, если мы делаем общий механизм с предоставлением API конечному пользователю, потому что неизвестно заранее, что пользователь захочет передавать в качестве аргументов и какие типы результата будут получены с удаленной стороны (иногда это неизвестно даже разработчику, пишущему поверх подобного API, потому что при цепочке вызовов аргументы следующего вызова часто основываются на результатах предыдущего).
В подобных случаях, когда базовый класс является не только интерфейсом для наследников, но и контейнером для данных наследника, мы получаем возможность описывать любой функционал, в котором требуется динамическая типизация в терминах классов и объектов C++.
Рассмотрим пример SQL-запроса. Список аргументов для выполнения запрос можно сгенерировать тем же Boost.Preprocessor для функции от произвольного числа аргументов типа object.
В качестве аргументов db::SqlQuery::operator() можно использовать произвольный набор object, в этом случае нужно определить шаблонный implicit конструктор приведения типов к общему типу object:
В этом случае нам потребуются наследники от класса object вида integer, boolean, floating, text, datetime и прочие, данные которых будут помещаться в object при инициализации объекта соответствующим значением. В этом случае инициализация объекта произвольным типом будет расширяемой и все, что нужно будет, чтобы задать объект нужным типом, — это написать соответствующую специализацию, наподобие этой для bool:
Самое важное здесь другое, результат выполнения запроса — таблица с данными, вычисленными на удаленной стороне базы данных в момент выполнения. Однако мы совершенно спокойно можем обходить каждую строку результата запроса, получая object совершенно определенного типа, а не какие-то недодесериализованные данные
С объектом можно работать, ему можно перегрузить операции сравнения, можно по аналогии с конструктором сделать шаблонный метод получения значения определенного типа, можно привести к строке, вывести в поток. Объект типа object у нас вполне себе контейнер, которым можно оперировать как обычным объектом класса.
Мало того, при желании можно добавить в object логику контейнера и обойтись вообще одним типом на любое значение, возвращаемое из запроса. То есть перегрузив ему методы begin(), end(), size(), а также operator [] :
В принципе, идею можно доработать до того, что можно вообще что угодно использовать через контейнер и базовый класс object, но здесь не стоит забывать о здравом смысле. Идея статической типизации, выявляющей ошибки еще на этапе компиляции, очень хороша, и отказываться от нее везде, где только можно надуманно использовать типизацию динамическую, крайне неразумно!
ВЫНОС ИЗ ТЕКСТА
Идея статической типизации, выявляющей ошибки еще на этапе компиляции, очень хороша, и отказываться от нее повсеместно крайне неразумно!
Тем не менее динамическая типизация крайне полезна в тех самых местах, для которых она предназначена, — для значений, чей тип получается динамически, как правило в результате разбора потока данных. Инкапсулированный в базовом классе интерфейс для работы с данными различного типа позволяет нам работать с обычными объектами C++, создавая и копируя их обычными конструкторами, и оператором присвоения, причем копирование можно сделать максимально отложенным (в идеале навсегда) с помощью техники copy-on-write.
В одно и то же время базовый класс является как классом интерфейса для работы с различными данными, так и контейнером. Для удобства можно определить для базового класса все необходимые операции: сравнения, индексации, математические и логические операции и операции с потоками. В целом можно реализовать максимально читаемый и логичный код, наиболее защищенный от ошибок хранения указателя на базовый класс, копирования и доступа из разных потоков. Особенно это полезно, если этот API разрабатывается под широкий круг задач, при работе с набором типов, которые изначально неизвестны, и сам набор типов может потенциально расширяться.
Совместимость типов
Механизм проверки согласования типов в языке со статической типизацией проверяет, что всякое выражение соответствует типу, ожидаемому тем контекстом, в котором оно присутствует. Например, в операторе присваивания вида выведенный для выражения тип должен соответствовать типу, который провозглашён или выведен для переменной . Нотация соответствия, называемая совместимостью, специфична для каждого языка.
Если и имеют единый тип, и присваивание разрешено для этого типа, то это выражение является корректным. Поэтому в простейших системах типов вопрос совместимости двух типов упрощается до вопроса их равенства (эквивалентности). Однако разные языки имеют разные критерии для определения совместимости типов двух выражений. Эти теории эквивалентности варьируются между двумя крайними случаями: <b>структурными системами типов</b> (англ. structural type system), в которых два типа эквивалентны, если описывают одинаковую внутреннюю структуру значения; и <b>номинативными системами типов</b> (англ. nominative type system), в которых синтаксически различные типы никогда не эквивалентны (то есть два типа равны только в том случае, если равны их идентификаторы).
В языках с правила совместимости более сложные. Например, если является подтипом , то значение, принадлежащее типу , может быть использовано в контексте, ожидающем значение типа , даже если обратное неверно. Как и в случае эквивалентности, отношения подтипов различаются в разных языках, и здесь возможно много вариантов правил. Наличие в языке параметрического или ситуативного полиморфизма может также влиять на совместимость типов.
Тип данных (тип)¶
Понятие тип данных является фундаментальным в теории программирования. Тип данных (или просто тип) — это характеристика определяющая множество значений и операций, которые могут быть выполнены над этими данными.
В зависимости от языка программирования, тип данных может хранить информацию о данных, к которым относятся поля, свойства, методы и другие структуры языка, а также о том, в каком месте, в стеке (stack) или куче (heap) будет выделяться память во время выполнения программы, а также её объем; в каких операциях (как, например, сложение , умножение , присваивание и т.д.) может участвовать тип данных.
Типы данных делятся на два вида:
- типы значения (value type) — хранят значение (их ещё называют значимыми типами)
- ссылочные типы (reference types) — хранят ссылку на значение.
При операции присваивания значения, принадлежащего к значимому типу, данные копируются (дублируются) в памяти. При операции присваивания значения, принадлежащему к ссылочным типам, копируется лишь ссылка на данные.
TypeScript¶
TypeScript — это язык программирования, разработанный в Microsoft. TypeScript является надмножеством JavaScript, имеет статическую систему типов и собственный компилятор. Статическая типизация позволяет отлавливать ошибки и баги во время компиляции, ещё до запуска приложения. Подробнее узнать о совместном использовании TypeScript и React можно .
Чтобы использовать TypeScript, нужно:
- добавить TypeScript в проект как зависимость.
- настроить компилятор.
- использовать правильные расширения файлов.
- установить файлы объявлений для используемых библиотек;
Остановимся подробнее на каждом из этих моментов.
Использование TypeScript вместе с Create React App
Create React App поддерживает TypeScript по умолчанию.
Чтобы создать новый проект с поддержкой TypeScript, используйте следующую команду:
Можно добавить поддержку TypeScript в уже существующий проект, как показано здесь.
Добавление TypeScript в проект
Всё начинается с одной единственной команды в терминале:
Или :
Ура! Вы установили последнюю версию TypeScript. Теперь в вашем распоряжении новая команда — . Но прежде, чем праздновать, давайте добавим соответствующий скрипт в файл :
Настройка компилятора TypeScript
Сам по себе компилятор бесполезен, пока мы не скажем, что именно ему нужно делать. Для этого есть специальный конфигурационный файл . Создадим этот файл:
Или :
Сгенерированный файл уже содержит несколько параметров, которые используются компилятором по умолчанию. Кроме того, можно указать множество опциональных параметров. Более детальная информация по каждому параметру находится здесь
Из всех параметров больше всего сейчас нас интересуют и . Очевидно, что компилятор берёт исходный TypeScript код, и компилирует его в JavaScript. И нам нужно, чтобы не возникло путаницы между исходными файлами и сгенерированным кодом.
Эту проблему можно решить в два шага:
Во-первых, изменим структуру проекта. Все файлы с исходниками переместим в директорию src.
Затем, укажем компилятору откуда ему брать исходные файлы и куда сохранять скомпилированный код.
Отлично! Теперь, если мы запустим скрипт сборки проекта, компилятор сохранит готовый JavaScript в директорию . В TypeScript React Starter уже есть готовый с неплохим набором параметров для дальнейшей тонкой настройки под себя.
Как правило, скомпилированный JavaScript-бандл не следует хранить в системе контроля версий, так что не забудьте добавить папку в файл .
Расширения файлов
В React мы почти всегда используем в качестве расширений файлов компонентов. В TypeScript лучше разделять файлы на два типа:
для файлов, содержащих разметку, и для всего остального.
Запуск TypeScript
Если всё было сделано правильно, можно попробовать скомпилировать TypeScript:
Или :
Если эта команда не вывела ничего в терминале, то процесс компиляции прошёл успешно.
Определения типов
Для анализа ошибок и выдачи всплывающих подсказок компилятор TypeScript использует файлы объявлений. Они содержат в себе всю информацию о типах, которые используются в конкретной библиотеке. В свою очередь это позволяет нам использовать JavaScript библиотеки в проекте совместно с TypeScript.
Существует два основных способа получения файлов объявлений:
Bundled — Библиотека устанавливается вместе с собственным файлом объявлений. Это прекрасный вариант для нас, так как всё, что нам нужно — установить нужный пакет. Чтобы проверить, есть ли у библиотеки файл объявлений, поищите в её исходных файлах. В некоторых библиотеках наличие и расположение этого файла указываются в в секциях или .
DefinitelyTyped — это внушительный репозиторий файлов объявлений. Например, React устанавливается без собственного файла объявления — вместо этого мы устанавливаем его отдельно:
Локальные объявления
Иногда пакет, который вы хотите использовать, не имеет ни собственного файла объявлений, ни соответствующего файла в репозитории DefinitelyTyped. В этом случае, мы можем объявить собственный локальный файл объявлений. Для этого надо создать файл в корне директории, где лежат исходники вашего проекта. Файл объявлений может выглядеть примерно так:
Подробнее о языках программирования со статической типизацией
Java – язык программирования, который использует объектно-ориентированный подход. Получил распространение благодаря мультиплатформенности. При компиляции код интерпретируется в байт-код, который может выполняться на любой операционной системе. Java и динамическая типизация несовместимы, так как язык строго типизирован.
Haskell – также один из популярных языков, код которого может интегрироваться в другие языки и взаимодействовать вместе с ними. Но, несмотря на такую гибкость, имеет строгую типизацию. Оснащен большим встроенным набором типов и возможностью создания собственных.
Совместимость типов (Type Compatibility), структурная типизация (structural typing)¶
Совместимость типов — это механизм, по которому происходит сравнение типов.
Простыми словами, совместимость типов — это совокупность правил, на основе которых программа, анализируя два типа данных, приходит к выводу, производить над ними операции, считая их совместимыми, либо они нуждаются в преобразовании.
Правила совместимости типов делятся на три вида, один из которых имеет название структурная типизация.
Структурная Типизация — это принцип, определяющий совместимость типов, основываясь не на иерархии наследования или явной реализации интерфейсов, а на их описании.
С определениями закончили, осталось закрепить конкретными примерами.
Сильная типизация в TypeScript проявляет себя в случаях, схожих с операцией сложения числа с массивом. В этом случае компилятор выбрасывает ошибки.
Статическая типизация в TypeScript проявляется в том, что к моменту окончания компиляции компилятор уже знает, к какому конкретному типу принадлежат все конструкции, нуждающиеся в аннотации типа.
В TypeScript, если тип не указывается явно, компилятор с помощью вывода типов выводит и указывает тип самостоятельно.
Несмотря на то, что и не имеют явно заданного общего предка, разрешает присваивать экземпляр класса переменной с типом (и наоборот).
В таких языках, как Java или C#, такое поведение недопустимо. В TypeScript это становится возможно из-за структурной типизации.
Так как совместимость типов происходит на основе описания типов, то в первом случае компилятор запоминает все члены типа и, если он находит аналогичные члены в типе , типы считаются совместимы. То же самое компилятор проделывает тогда, когда во втором случае присваивает экземпляр класса переменной с типом . Так как оба типа имеют по одному полю, с одинаковым типом и идентификатором, то они считаются совместимыми.
Если добавить классу поле , то при попытке присвоить его экземпляр переменной с типом возникнет ошибка, так как в типе отсутствует после . Обратное действие, то есть присвоение экземпляра класса переменной с типом , ошибки не вызовет, так как в типе будут найдены все члены, объявленные в типе .
Стоит добавить, что правилам структурной типизации подчиняются все объекты в TypeScript. А, как известно, в JavaScript все, кроме примитивных типов, объекты. Это же утверждение верно и для TypeScript.
С первыми четырьмя пунктами разобрались. Двигаемся дальше.