Работа с типами в процессе компиляции может быть разбита на две категории:
Типы имеют структуру. Будем описывать её выражениями типов.
Выражение типа:
Примитивные (фундаментальные) типы – имена типов, определённые стандартом языка.
Не могут быть представлены в терминах других типов.
Например, в C
: int
, bool
, float
, void
и т.п.
Конструкторы типов – “функции” над выражениями типов.
Например int[3]
– массив int
длины 3 – это конструктор array
, применённый к аргументам 3
и int
Конструктор ->
– функции
Например, тип int floor(float x)
– float -> int
Для многих аргументов – пары/кортежи.
Например, void(int, float)
записывается как int × float -> void
×
– конструктор пары.
Эквивалентно: (int, float) -> void
.
Для представления выражений типов удобно использовать синтаксические деревья или ориентированные ациклические графы.
int × float -> void
:
Когда два выражения типов эквивалентны?
Два подхода:
Обычно – оба подхода в разных контекстах.
Например, пользовательские типы (в ООП – классы) – именная эквивалентность.
не эквивалентны:
эквивалентны:
Реализованная в полной мере структурная эквивалентность позволяет взаимозаменяемо использовать типы, имеющие (на каком-то уровне) одинаковое представление.
Такой вариант – в TypeScript (структурная эквивалентность в представлении JavaScript)
На практике, в основном – именная эквивалентность. Структурная – синонимы типов.
Причина – именная эквивалентность проще.
Для проверки типов:
Система типов – набор логических правил.
Проверка типов – способ обнаружения ошибок в программах.
Выполняется:
Согласованная или надёжная система типов – все проверки возможны статически.
Строгая система типов – гарантия отсутствия ошибок типов.
Два вида правил проверки:
Синтез типов – тип выражения определяется в терминах его подвыражений.
Выведение типов – определяет типы использованию, т.е. выводит типы выражений из контекста.
Например, если функция length
, принимает список, то, из выражения length(x)
вывод, что x
– список (с произвольным типом элемента).
Операции над выражениями различных типов – необходимо преобразование.
Например, сложение float
и int
– нет машинной операции. Необходимо преобразовать int
в float
(или float
в int
)
Такие преобразования называются приведением типов.
Приведение типов:
Также:
Пусть есть типы int16
и int32
, знаковые целые соответственно в 16 и 32 битах.
Тогда:
int16
в int32
– расширяющееint32
в int16
– сужающееОбычно неявные приведения – расширяющие.
Какие расширяющие, какие сужающие – зависит от языка.
Например, в С++ (стрелки – расширяющие):
В ООП – преобразования производных классов к базовым – расширяющие.
С точки зрения теории множеств, преобразования, к типу, имеющему больше возможных значений – расширяющие, наоборот – сужающие.
Перегруженная функция – различное определение в зависимости от контекста.
Также – аd-hoc полиморфизм.
Реализуется объявлением нескольких функций с одинаковым названием, но с разными реализациями.
Выбор конкретной реализации
Термин “полиморфность” – фрагмент кода может быть выполнен для различных типов.
Функция length
на Haskell (например):
Работает для любых списков.
Чтобы записать тип length
– универсальная квантификация:
length : ∀α. list(α) -> integer
list – конструктор типа списка
->
– конструктор типа функции
α
– переменная типа
∀α.
– универсальный квантификатор.
При использовании – связанная переменная типа имеет разное значение (тип).
В процессе проверки типов – универсальная квантификация убирается, переменная заменяется на конкретный тип.
Необходимо пересмотреть понятие эквивалентности типов.
Если e1 : a -> b
применяется к e2 : c
, то вместо эквивалентности a
и c
мы должны их унифицировать.
Например, при вызове
где [1,2,3] : list(integer)
∀α. list(α)
с list(integer)
α
, дающую эквивалентностьЗдесь подходит α ⇒ integer
, и length : list(integer) -> integer
.
Для выражений типов s
и t
, алгоритм унификации определяет, можно ли сделать их идентичными какой-то подстановкой.
Если в s
и t
нет переменных, то унификация ≡ идентичность.
Рассмотрим алгоритм унификации типов на графах.
Узлы группируются в классы эквивалентности.
Если два узла принадлежат одному классу, то их выражения типов унифицируются.
Изначально каждый узел – собственный класс эквивалентности.
Классы эквивалентности – через указатели на представителя класса.
Каждый узел имеет свойство set
– указатель на узел или 0
.
Представитель класса эквивалентности – один произвольно выбранный узел в подграфе выражения, у него set = 0
.
Определим find(n)
– возвращает представителя класса, содержащего узел n
.
Определим union(m,n)
– объединяет классы эквивалентности, содержащие узлы m
и n
.
Алгоритм:
boolean unify(Node m, Node n) {
s = find(m);
t = find(n);
if (s == t) return true;
else if (equivalent(s, t)) {
// s и t эквивалентные фундаментальные типы
// или эквивалентные конструкторы типа
if(!s.hasChildren && !t.hasChildren) {
// s и t -- фундаментальные
return true;
}
union(s, t);
return unify(s[0], t[0]) && unify(s[1], t[1]);
} else(s.isVar || t.isVar) {
// s или t -- переменная
union(s,t);
return true;
} else return false;
}
Пример:
((α -> β) × list(γ)) -> list(β)
((γ -> δ) × list(γ)) -> list(ζ)
В виде графа:
Класс – после двоеточия.
В результате после завершения алгоритма получаем следующие классы эквивалентности:
Алгоритм унификации здесь завершается успешно.