ФП 2.0 · осень 2026
осень 2026
TYPE, representation polymorphism| Что сегодня | Где выстрелит |
|---|---|
| универсальная аппликация, кайнды | словари классов типов и семейства типов — глава 3 |
| higher-kinded полиморфизм | Fix и свёртки — глава 4; Free — глава 7 |
| типы высшего ранга | tagless final — глава 5; ST, хендлеры — главы 6–7 |
| GADT и структуры на уровне типов | типизированные интерпретаторы — глава 5; системы эффектов — глава 8 |
| стирание типов и словари | реализация классов типов — глава 3 |
Корректность — семантическое свойство программы. По теореме Райса алгоритмически её границу не найти.
Разрешимая система типов очерчивает область поменьше: вместе с некорректными программами она отвергает много корректных.
Параметрический полиморфизм расширяет область: один код единообразно работает с любыми типами.
Сегодня расширяем её дальше: ранги, higher-kinded, GADT, структуры на уровне типов. Граница всё равно останется внутри зелёной.
Один id — три места вызова. Кто и когда выбирает конкретный тип вместо a?
id x = x
id 42
id 'x'
id True
Вызывающий код, в каждом месте свой: у id есть ещё один, скрытый аргумент — сам тип.
id x = x
id @Int 42
id @Char 'x'
id @Bool True
И в определении его можно принять явно: функция двух аргументов — типа и значения.
id @a (x :: a) = x
id @Int 42
id @Char 'x'
id @Bool True
System F: \Lambda\alpha обобщает выражение по типу, аппликация к типу его подставляет. Получается редекс, как у обычной функции.
(Λα. λx:α. x) nat 42
Тип подставлен в аннотацию параметра: \alpha \mapsto \mathrm{nat}. Осталась обычная функция и обычный β-редекс.
(λx:nat. x) 42
Вызов id @Int 42 в Haskell — те же два шага: сначала выбран тип, потом передано значение.
42
Обобщаем по значению: λ добавляет стрелку, аппликация снимает
\dfrac{x : \tau,\ \Gamma \vdash M : \sigma}{\Gamma \vdash \lambda x : \tau.\,M : \tau \to \sigma}\ {\scriptstyle Lam}
\dfrac{\Gamma \vdash M : \tau \to \sigma \quad \Gamma \vdash N : \tau}{\Gamma \vdash M\ N : \sigma}\ {\scriptstyle App}
Обобщаем по типу: Λ добавляет \forall, аппликация к типу снимает
\dfrac{\alpha,\ \Gamma \vdash M : \tau}{\Gamma \vdash \Lambda\alpha.\,M : \forall\alpha.\,\tau}\ {\scriptstyle TLam}
\dfrac{\Gamma \vdash M : \forall\alpha.\,\tau}{\Gamma \vdash M\ \sigma : [\alpha \mapsto \sigma]\ \tau}\ {\scriptstyle TApp}
id получает тип, потом значение\mathbf{id} = \Lambda\alpha.\,\lambda x : \alpha.\,x, тип \mathbf{id} : \forall\alpha.\,\alpha\to\alpha
\vdash \mathbf{id} : \forall\alpha.\,\alpha\to\alpha def
\vdash \mathbf{id}\ \mathrm{nat} : \mathrm{nat}\to\mathrm{nat} TApp
\vdash 42 : \mathrm{nat} const
\vdash \mathbf{id}\ \mathrm{nat}\ 42 : \mathrm{nat} App
шаги
1. App: аппликация — две посылки
2. TApp: снимаем ∀, α ↦ nat; const: 42
3. def: тип id по определению
Какой тип у выражения const @Int? А у const @Int @Char? От чего зависит порядок, в котором передаются типы?
Синоним с параметрами — абстракция, только на типах
\mathbf{Pair} = \lambda \tau\,\sigma.\,\forall \gamma.\,(\tau \to \sigma \to \gamma) \to \gamma
Применение абстракции к типу редуцируется подстановкой, как на уровне термов
(\lambda\alpha.\,\tau)\ \sigma \ \rightsquigarrow\ [\alpha \mapsto \sigma]\ \tau
Что произошло в GHCi? Применение синонима к аргументам — выражение на уровне типов, и его можно вычислять.
Pair Int Int
Раскрываем имя \mathbf{Pair}: получаем аппликацию типовой абстракции к двум аргументам — редекс на уровне типов.
(λτ σ. ∀γ. (τ → σ → γ) → γ) Int Int
Первый β-шаг на уровне типов: \tau \mapsto \mathrm{Int}; остался редекс с \sigma.
(λσ. ∀γ. (Int → σ → γ) → γ) Int
Второй β-шаг: \sigma \mapsto \mathrm{Int}. Нормальная форма — полиморфный тип, у которого квантор остался внутри: пара — функция, ждущая продолжение.
∀γ. (Int → Int → γ) → γ
Этаж термов: вершинам дерева терма Just True приписаны типы — пока как записи.
Just True
Но тип — тоже дерево: у Just это стрелка с поддеревьями Bool и аппликацией Maybe к Bool; у True — дерево из одного листа.
Just True
Этаж типов: теперь уже вершинам деревьев типов приписаны кайнды. Аппликация типа к типу снимает стрелку в кайнде, как аппликация терма снимает стрелку в типе.
Just True
И кайнды — тоже деревья: Type -> Type у Maybe — стрелка из двух листьев Type, остальные — деревья из одного листа. Что приписывается их вершинам — в конце раздела.
Just True
λ-исчисление на типах
\dfrac{\alpha : \kappa_1,\ \Gamma \vdash \tau : \kappa_2}{\Gamma \vdash \lambda\alpha : \kappa_1.\,\tau : \kappa_1 \to \kappa_2}\ {\scriptstyle KLam}
\dfrac{\Gamma \vdash \tau : \kappa_1 \to \kappa_2 \quad \Gamma \vdash \sigma : \kappa_1}{\Gamma \vdash \tau\ \sigma : \kappa_2}\ {\scriptstyle KApp}
Конструкции обычных типов
\dfrac{\Gamma \vdash \tau : * \quad \Gamma \vdash \sigma : *}{\Gamma \vdash \tau \to \sigma : *}\ {\scriptstyle KArrow}
\dfrac{\alpha : \kappa,\ \Gamma \vdash \tau : *}{\Gamma \vdash \forall\alpha : \kappa.\,\tau : *}\ {\scriptstyle KAll}
\begin{array}{l} \mathbf{Pair} : * \to * \to * \\ \mathbf{Pair} = \lambda \tau^*\,\sigma^*.\,\forall \gamma.\,(\tau \to \sigma \to \gamma) \to \gamma \\[0.4em] \mathbf{pair} : \forall \alpha\,\beta.\,\alpha \to \beta \to \mathbf{Pair}\ \alpha\ \beta \\ \mathbf{pair} = \Lambda \alpha^*\,\beta^*.\,\lambda x^\alpha\,y^\beta.\,(\Lambda \gamma^*.\,\lambda f^{\alpha \to \beta \to \gamma}.\,f\ x\ y) \\[0.4em] \mathbf{fst} : \forall \alpha\,\beta.\,\mathbf{Pair}\ \alpha\ \beta \to \alpha \\ \mathbf{fst} = \Lambda \alpha^*\,\beta^*.\,\lambda p^{\mathbf{Pair}\ \alpha\ \beta}.\,p\ \alpha\ (\mathbf{K}\ \alpha\ \beta) \end{array}
Достанем первую компоненту пары. Типы здесь — такие же аргументы, как значения.
fst nat bool (pair nat bool 1 true)
Раскрыли \mathbf{fst} и передали типы компонент: \alpha \mapsto \mathrm{nat}, \beta \mapsto \mathrm{bool}.
(λp. p nat (K nat bool)) (pair nat bool 1 true)
Пара подставлена вместо p. Дальше она ждёт тип результата и функцию.
(pair nat bool 1 true) nat (K nat bool)
Раскрыли \mathbf{pair}: внутри полиморфная функция, и первым она принимает тип результата \gamma.
(Λγ. λf. f 1 true) nat (K nat bool)
\gamma \mapsto \mathrm{nat}: пара узнала, что из неё хотят достать число.
(λf. f 1 true) (K nat bool)
Функция-выборщик получила обе компоненты.
K nat bool 1 true
\mathbf{K} вернула первую. Каждый квантор в типах \mathbf{fst} и \mathbf{Pair} — это шаг, на котором кто-то выбрал тип.
1
Постройте по правилам кайндизации дерево вывода \mathbf{Pair} : * \to * \to *. Какие правила и в каком порядке в нём применяются? Поредуцируйте тип \mathbf{Pair}\ \mathrm{nat}\ \mathrm{bool} до нормальной формы.
Реализуйте на Haskell функции snd и swap :: Pair a b -> Pair b a. Запишите snd в исчислении с явными типовыми абстракциями и аппликациями.
Дерево вывода — на следующем слайде. Редукция — два β-шага, как у \mathbf{Pair}\ \mathrm{Int}\ \mathrm{Int}: нормальная форма \forall\gamma.\,(\mathrm{nat}\to\mathrm{bool}\to\gamma)\to\gamma.
snd :: Pair a b -> b
snd p = p (\_ y -> y)
swap :: Pair a b -> Pair b a
swap p = \f -> p (\x y -> f y x) -- или pair (snd p) (fst p)\mathbf{snd} = \Lambda \alpha^*\,\beta^*.\,\lambda p^{\mathbf{Pair}\ \alpha\ \beta}.\,p\ \beta\ (\mathbf{K}_*\ \alpha\ \beta)
Pair\Gamma = \tau : *,\ \sigma : *,\ \gamma : *
\Gamma \vdash \tau : * ctx
\Gamma \vdash \sigma : * ctx
\Gamma \vdash \gamma : * ctx
\Gamma \vdash \sigma\to\gamma : * KArrow
\Gamma \vdash \tau\to\sigma\to\gamma : * KArrow
\Gamma \vdash \gamma : * ctx
\Gamma \vdash (\tau\to\sigma\to\gamma)\to\gamma : * KArrow
\tau : *,\ \sigma : * \vdash \forall\gamma.\,(\tau\to\sigma\to\gamma)\to\gamma : * KAll
\tau : * \vdash \lambda\sigma.\,\forall\gamma.\,(\tau\to\sigma\to\gamma)\to\gamma : *\to* KLam
\vdash \lambda\tau\,\sigma.\,\forall\gamma.\,(\tau\to\sigma\to\gamma)\to\gamma : *\to*\to* KLam
шаги
1. KLam: снимаем λτ
2. KLam: снимаем λσ
3. KAll: снимаем ∀γ
4. KArrow: внешняя стрелка
5. KArrow: τ → (σ → γ); ctx: γ
6. ctx: τ; KArrow: σ → γ
7. ctx: σ и γ
Программа корректна: передай в неё id — получишь пару. Выведем её тип, как это делает алгоритм Хиндли–Милнера.
λg. (g 42, g 'a')
Аргументу функции алгоритм приписывает один тип \gamma, без кванторов внутри.
λg. (g 42, g 'a')
Первое применение: g применяют к числу, значит \gamma = \mathrm{Int}\to\beta_1.
λg. (g 42, g 'a')
Второе: \gamma = \mathrm{Char}\to\beta_2. Но \gamma уже занята, а \mathrm{Int} \neq \mathrm{Char} — корректная программа отвергнута.
λg. (g 42, g 'a')
Лекарство — квантор слева от стрелки: тип для g выбирает сама функция, в каждом месте свой. Это тип высшего ранга.
λg. (g 42, g 'a')
Какую ошибку выдаст GHC, если убрать у f сигнатуру и типовые аппликации? Какой тип для g он пытался вывести?
Какой ранг имеет тип Int -> (forall a . a -> a)?
Couldn't match expected type ‘Int’ with actual type ‘Char’: первое применение зафиксировало g :: Int -> b. Про ранг — на следующем слайде.
Тип — дерево. Ранг считает, сколько раз по пути к квантору мы свернули налево от стрелки. Здесь один раз: ранг 2.
(∀a. a → a) → (Int, Char)
А в этом типе квантор стоит справа от стрелки…
Int → (∀a. a → a)
…и его можно вынести наружу, ничего не изменив. Поворотов налево нет: ранг 1.
∀a. Int → a → a
Оператор $ — обычная функция. Чему равна типовая переменная a в выражении runST $ do …?
($) :: (a → b) → a → b
runST :: (∀s. ST s c) → c
a специализируется на полиморфный тип. По умолчанию так нельзя; для $ в GHC сделано исключение, для остальных функций — ImpredicativeTypes.
($) :: ((∀s. ST s c) → c) → (∀s. ST s c) → c
runST :: (∀s. ST s c) → c
Квантор по s стоит слева от стрелки: его область действия — только аргумент runST.
runST :: (∀s. ST s a) → a
Вернуть ссылку — значит взять a = \mathrm{STRef}\ s\ \mathrm{Bool}. Но тогда s в результате оказывается вне области квантора: такого типа не существует.
runST :: (∀s. ST s (STRef s Bool)) → STRef s Bool
Один fmap для списков, Maybe и деревьев: переменная f стоит на месте конструктора типов.
fmap :: (a → b) → f a → f b
f :: Type → Type
Подставляем список…
fmap :: (a → b) → [] a → [] b
[] :: Type → Type
…Maybe…
fmap :: (a → b) → Maybe a → Maybe b
Maybe :: Type → Type
…или дерево. В Haskell абстрагироваться можно по типам любых кайндов — и в функциях, и в data.
fmap :: (a → b) → Tree a → Tree b
Tree :: Type → Type
Какой кайнд у Fix Maybe? Приведите несколько значений этого типа. Какой знакомый тип они напоминают?
Fix Maybe :: Type; значения — Fix Nothing, Fix (Just (Fix Nothing)), Fix (Just (Fix (Just (Fix Nothing)))), … — натуральные числа Пеано.
data Expr = Const Int | IsZero Expr | If Expr Expr Expr. Типу Expr всё равно, что внутри: каждый узел — Expr, значение Haskell корректно. А программа бессмысленна.
IsZero (IsZero (Const 1))
Добавим типу индекс — тип, в который выражение вычисляется. Теперь IsZero ждёт число, а получает булево: ошибка видна в типах.
IsZero (IsZero (Const 1))
eval обязательнаДобавьте в язык пары: конструкторы MkPair :: Expr a -> Expr b -> Expr (a, b) и Fst :: Expr (a, b) -> Expr a. Допишите eval. Какие равенства типов возникают в новых ветках?
Сколько обитателей у типа Suc (Suc Zero)?
Ни одного (кроме \bot): типы пустые, они нужны только как индексы.
Пустой вектор: длина — ноль.
VNil
:: Vec Zero a
Каждый VCons добавляет элемент в значение и Suc в тип.
VCons 2 VNil
:: Vec (Suc Zero) Int
Длина вектора известна компилятору.
VCons 1 (VCons 2 VNil)
:: Vec (Suc (Suc Zero)) Int
Напишите функцию добавления элемента в конец вектора. Двигайтесь последовательно, заполняя типовые дыры и отслеживая возникающие эквивалентности.
Suc :: Type -> Type, и индексом вектора может оказаться любой типЧто выведет ghci> :k Vec?
Vec :: Nat -> Type -> Type
Обычное объявление data Nat = Zero | Suc Nat: тип Nat и его значения.
DataKinds продвигает объявление на этаж выше: конструктор типа становится кайндом, конструкторы данных — типами.
| Term | Type | Kind |
|---|---|---|
Zero |
Nat |
Type |
[Zero, Suc Zero] |
[Nat] |
Type |
[] |
forall a. [a] |
Type |
(:) |
forall a. a -> [a] -> [a] |
Type |
'Suc 'Zero |
Nat |
|
'['Zero, 'Suc 'Zero] |
[Nat] |
|
'[Int, Double] |
[Type] |
|
'[] |
forall k. [k] |
|
'(:) |
forall k. k -> [k] -> [k] |
Конвенция вызова: где лежит аргумент — регистр, стек, куча — и какого он размера. Она зависит от типа, а тип T неизвестен.
Мономорфизация (C++, Rust): копия кода на каждый набор типов. Самый быстрый код; долгая компиляция, большой бинарник, нет полиморфных методов в интерфейсах.
Стирание типа (JVM, Haskell): значения — в кучу, код один и ждёт указатели. Быстрая компиляция и гибкость; платим боксингом и индирекциями.
Гибрид (C#): для ссылочных типов один код с указателями, для value-типов специализации появляются лениво, в рантайме.
Таблица свойств типа (Swift): код один, а размер и способ копирования приходят вместе с типом — в таблице. Платим вызовами через таблицу.
| мономорфизация | стирание | гибрид | таблица свойств | |
|---|---|---|---|---|
| языки | C++, Rust | JVM, Haskell | C# | Swift |
| скорость кода | + лучшая | − боксинг | + value-типы | − вызовы через таблицу |
| время компиляции, размер | − | + | + | + |
| динамическая загрузка | − | + | + | + |
| полиморфные методы, higher-rank | − | + | + | + |
| тип известен в рантайме | − только статически | − стёрт | + | + |
В Kotlin (JVM, стирание типов) проверка x is T для типового параметра T разрешена только внутри inline-функций, где параметр помечен reified. Объясните ограничение. Какой из подходов здесь локально применяется?
В Rust (мономорфизация) трейт с обобщённым методом нельзя использовать как trait object (dyn Trait). Каким недостатком мономорфизации это объясняется?
T стёрт; inline вставляет тело в место вызова, где T известен, — локальная мономорфизацияПриложение подгружает плагин, скомпилированный отдельно, и хочет вызвать из него обобщённую функцию с типом, о котором автор плагина не знал. Для каждого из четырёх подходов определите, возможно ли это, и почему.
x :: Int — boxed: в переменной указатель, само число в куче. Обычный Int — это data Int = I# Int#.
И lifted: по указателю может лежать отложенное вычисление, ещё не число.
Когда значение понадобится, вычисление заменится результатом. За лень и единообразие платим индирекцией и аллокацией.
z :: Int# — unboxed и unlifted: число прямо в регистре, без кучи и без лени.
Int :: TYPE (BoxedRep Lifted), или просто TypeInt# :: TYPE IntRepMaybe Int :: TypeMaybe :: Type -> Type(# Int, Bool #) :: TYPE (TupleRep '[LiftedRep, LiftedRep])(# Int#, (# Int, Double# #) #) :: TYPE (TupleRep '[IntRep, TupleRep '[LiftedRep, DoubleRep]])Какой кайнд у типа (# Int#, Bool #)? Проверьте себя в GHCi, включив расширения MagicHash и UnboxedTuples.
(# Int#, Bool #) :: TYPE (TupleRep '[IntRep, LiftedRep])
errorФункция error полиморфна по представлению результата: error :: forall (r :: RuntimeRep) (a :: TYPE r). String -> a. Почему это не нарушает ограничение на связыватели?
a стоит только в результате: ни один параметр error не имеет тип a, связывателя полиморфного представления нет. А результат никогда не строится — функция бросает исключение, так что и код под произвольное представление генерировать не нужно.
| System F | скрытый аргумент — тип; \Lambda и универсальная аппликация, TLam/TApp; в Haskell — @ |
| кайнды | λ-исчисление на уровне типов, KLam/KApp; три этажа: терм — тип — кайнд, и Type :: Type |
| формы полиморфизма | высшие ранги и импредикативность (first-class), higher-kinded, GADT, продвижение данных, полиморфные кайнды |
| реализация | мономорфизация, стирание, гибрид, witness table — у каждого своя цена; словари вернутся в главе 3 |
| представления | boxed/unboxed, lifted/unlifted, TYPE r; связыватели не бывают полиморфны по представлению |
Expr; формулы на уровне типов, hzip, Tagged, числа и пара Чёрча; тип errorProxyЛекция 2. Параметрический полиморфизм