Классы типов в языке: словари, неявные аргументы, вывод инстансов, когерентность, дефункционализация
Семейства: выбор по типу переносится с кода на данные и на сами типы
Кайнд Constraint: ограничения как типы, с которыми можно вычислять
Использование: что языки выражают через классы типов — от экзистенциальных типов до исключений
Где мы на карте
Что в этой главе
Откуда пришло и где выстрелит
словари классов типов
таблица свойств типа в Swift — глава 2; здесь набор операций задаёт программист
неявные аргументы
динамическое связывание — глава 5; системы эффектов — глава 8
дефункционализация
продолжения превращаются в стек фреймов — глава 6
семейства и Constraint
вычисления в типах из домашки 2; datatype-generic programming — глава 9
равенства типов, System FC
как внутри устроены GADT из главы 2
экзистенциальные типы, исключения
эффекты и хендлеры — главы 7–8
Перегрузка и классы типов
std::string toString(int x){...}// перегрузка: одно имя, несколько функцийstd::string toString(std::string fmt,double d){...}
classShow a where-- сначала объявляем, что перегружается и по какому параметру show :: a ->StringinstanceShowIntwhereshow=...
showPrefixed ::Show a => a ->String->String-- требуется словарь Show a
Выбор по типу, а не по значению
ghci> minBound ::Int-- константа в классе: значения-получателя нет вовсе-9223372036854775808ghci>pure1 ::MaybeInt-- выбор по результату, и даже не по типу — по конструкторуJust1ghci>pure1 :: [Int][1]
ghci>:k ShowShow ::Type->Constraint-- класс — не тип, а предикат на типахghci>:k [Show]error:Expecting one more argument to ‘Show’Expected a type, but ‘Show’ has kind ‘Type->Constraint’
Кто выбирает код
Параметрический полиморфизм: один и тот же код работает с данными любых типов.
length [1, 2]
length 'ab'
length [True]
length :: [a] -> Int
Кто выбирает код
Специальный (ad-hoc) полиморфизм: имя одно, а код выбирается по типу.
show 42
show 'x'
show True
show для Int
show для Char
show для Bool
Кто выбирает код
А если вызов стоит внутри полиморфной функции? Тип a неизвестен — какой из трёх show вызывать?
showAll [1, 2]
showAll 'ab'
showAll [True]
showAll xs = … show x …
show для Int
show для Char
show для Bool
какой из трёх?
Кто выбирает код
Код приезжает вместе с вызовом: скрытым аргументом передаётся таблица функций для типа. Это и есть класс типов.
showAll [1, 2]
showAll 'ab'
showAll [True]
словарь Show Int
словарь Show Char
словарь Show Bool
showAll d xs = … show d x …
Классы типов в языке
Что сортировка знает о типе
Сортировка вставками для чисел. От типа элементов ей нужно одно — порядок.
sort ::[Int] → [Int]
insert x xs = … partition(< x)xs …
Что сортировка знает о типе
Обобщаем по типу элементов. Параметрически-полиморфный код не знает про a ничего: сравнивать нечем.
sort ::[a] → [a]
insert x xs = … partition(? x)xs …
Что сортировка знает о типе
Значит, порядок придётся принять аргументом: словарь функций для типа a.
sort ::OrdDict a →[a] → [a]
insert x xs = … partition(`less` x)xs …
Словарь — обычное значение
dataOrdDict a =OrdDict { less :: a -> a ->Bool }intOrd ::OrdDictIntintOrd =OrdDict { less = (<) }ghci>sort intOrd [3, 2, 1][1,2,3]
listOrd ::OrdDict a ->OrdDict [a] -- порядок на списках — из порядка на элементахlistOrd d =OrdDict { less =... less d ... }ghci>sort (listOrd intOrd) [[3, 2], [2, 1], [0]][[0],[2,1],[3,2]]
Задача: порядок на списках
Допишите listOrd так, чтобы списки сравнивались лексикографически.
listOrd ::OrdDict a ->OrdDict [a]listOrd d =OrdDict { less = go }where go _ [] =False go [] _ =True go (x:xs) (y:ys) = less d x y || (not (less d y x) && go xs ys)
Компилятор подставляет словарь
Тот же код с классом типов. Словарей не видно.
sort ::Ord a ⇒[a] → [a]
sort[3, 2, 1]
sort[[3, 2], [2, 1], [0]]
Компилятор подставляет словарь
Компилятор превращает => в обычную стрелку, а в каждом месте вызова строит словарь по типу аргумента.
sort ::OrdDict a →[a] → [a]
sortintOrd[3, 2, 1]
sort(listOrd intOrd)[[3, 2], [2, 1], [0]]
Население типа словаря
Каждый инстанс даёт словарь или функцию, строящую словарь. Нужно населить OrdDict [Int].
Головы инстансов
Типы словарей и функций
instance Ord Int
intOrd
:: OrdDict Int
instance Ord Bool
boolOrd
:: OrdDict Bool
instance Ord a ⇒ Ord [a]
listOrd
:: ∀a. OrdDict a → OrdDict [a]
Ожидаемый тип словаря
Терм, который строит компилятор
OrdDict [Int]
□
Население типа словаря
Смотрим под OrdDict: снаружи у [Int] конструктор []. Подходит голова Ord [a].
Головы инстансов
Типы словарей и функций
instance Ord Int
intOrd
:: OrdDict Int
instance Ord Bool
boolOrd
:: OrdDict Bool
instance Ord a ⇒ Ord [a]
listOrd
:: ∀a. OrdDict a → OrdDict [a]
Ожидаемый тип словаря
Терм, который строит компилятор
OrdDict [Int]
[]
конструктор списка
□
Население типа словаря
Сопоставление [a] с [Int] даёт a = Int. Подставляем его во весь тип listOrd.
Головы инстансов
Типы словарей и функций
instance Ord Int
intOrd
:: OrdDict Int
instance Ord Bool
boolOrd
:: OrdDict Bool
instance Ord a ⇒ Ord [a]
listOrd
:: OrdDict Int → OrdDict [Int]
Ожидаемый тип словаря
Терм, который строит компилятор
OrdDict [Int]
a = Int
□
Население типа словаря
Результат listOrd подходит. Его аргумент становится новой целью: OrdDict Int.
Головы инстансов
Типы словарей и функций
instance Ord Int
intOrd
:: OrdDict Int
instance Ord Bool
boolOrd
:: OrdDict Bool
instance Ord a ⇒ Ord [a]
listOrd
:: OrdDict Int → OrdDict [Int]
Ожидаемый тип словаря
Терм, который строит компилятор
OrdDict [Int]
OrdDict Int
@
listOrd
□
Население типа словаря
У новой цели снаружи Int. Подходит Ord Int: словарь intOrd не требует аргументов.
Получили listOrd intOrd :: OrdDict [Int]. Именно этот аргумент компилятор передаёт в sort.
Головы инстансов
Типы словарей и функций
instance Ord Int
intOrd
:: OrdDict Int
instance Ord Bool
boolOrd
:: OrdDict Bool
instance Ord a ⇒ Ord [a]
listOrd
:: OrdDict Int → OrdDict [Int]
Ожидаемый тип словаря
Терм, который строит компилятор
OrdDict [Int]
OrdDict Int
listOrd
intOrd
дальше — обычная передача аргумента
Класс типов и словарь — одно и то же
Словарь: всё руками
-- 1. набор функцийdataMyOrd a =MyOrd { less :: a -> a ->Bool }-- 2. экземпляр: значение с именемintMyOrd ::MyOrdIntintMyOrd =MyOrd { less = (<) }-- 3. явный параметрsort ::MyOrd a -> [a] -> [a]-- 4. передаёт программистtest =sort intMyOrd [3, 2, 1]
Класс типов: то же делает компилятор
-- 1. набор функцийclassMyOrd a where less :: a -> a ->Bool-- 2. экземпляр: имени нетinstanceMyOrdIntwhere less = (<)-- 3. неявный параметрsort ::MyOrd a => [a] -> [a]-- 4. передаёт компиляторtest =sort [3, 2, 1]
ΔСвидетель (witness) — словарь как доказательство того, что тип удовлетворяет ограничению
Задача: словарь для Functor
Какой словарь будет соответствовать higher-kinded классу типов Functor?
dataFunctorDict f =FunctorDict { fmap ::forall a b . (a -> b) -> f a -> f b } -- поле — полиморфная функция: ранг 2maybeFunctor ::FunctorDictMaybe
=> — стрелка с неявным аргументом
f :: (Show b => b) -> bf x = xerror:Noinstance for ‘Show b’ arising from a use of ‘x’Possible fix: add (Show b) to the context of the type signature for: f ::forall b. (Show b => b) -> b
f ::Show b => (Show b => b) -> bf x = x
Словарь доходит до места использования
Аргумент x — функция от словаря. В теле f его надо к чему-то применить, а словаря в области видимости нет.
f ::(ShowDict b → b) → b
fx =x?
Словарь доходит до места использования
Примем словарь аргументом f — и компилятор сам передаст его дальше.
f ::ShowDict b →(ShowDict b → b) → b
fdx =xd
Неявным может быть любой аргумент
sort :: (?cmp :: a -> a ->Bool) => [a] -> [a] -- ImplicitParamssort= sortBy ?cmpghci>let?cmp = (>) insort [1, 3, 2] -- берётся из области видимости по имени[3,2,1]
dataShowDict a whereShowDict ::Show a =>ShowDict a -- словарь, сохранённый в структуре данныхf ::ShowDict b -> (Show b => b) -> bf d x =case d ofShowDict-> x -- в ветке инстанс Show b доступен
Задача: когда приходит словарь
Аргумент x имеет тип Show b => b, то есть ждёт словарь. В какой момент он его получает? Требует ли такая передача словаря ленивости языка, или достаточно передать функцию от словаря? Что вычисляется до вызова f в энергичном языке?
В месте использования, внутри ветки case: x применяется к словарю из обёртки. После рассахаривания x — функция от словаря. В энергичном языке до вызова достаточно получить её замыкание; тело выполнится при применении. Передача по имени не требуется.
Рекурсия по структуре типа
typedataNat=Zero|SucNatclassKnownNat (n ::Nat) where natVal ::Int-- AllowAmbiguousTypes: n нет в типе методаinstanceKnownNatZerowhere natVal =0instanceKnownNat n =>KnownNat (Suc n) where natVal =1+ natVal @n
ghci> natVal @(Suc (SucZero))2
Как словарь превращается в число
natVal @(Suc (Suc Zero)) превращается в вызов natValD со словарём. В словаре одно поле — число.
natValD□
data KnownNatDict (n :: Nat) = KN { natValD :: Int }
knownZero = KN { natValD = 0 }
knownSuc d = KN { natValD = 1 + natValD d }
Тип словаря
Терм словаря
KnownNatDict (Suc (Suc Zero))
Как словарь превращается в число
Компилятор выбирает инстанс для Suc n: вставляет knownSuc и ищет словарь для n = Suc Zero.
natValD (knownSuc□)
data KnownNatDict (n :: Nat) = KN { natValD :: Int }
knownZero = KN { natValD = 0 }
knownSuc d = KN { natValD = 1 + natValD d }
Тип словаря
Терм словаря
KnownNatDict (Suc (Suc Zero))
KnownNatDict (Suc Zero)
Как словарь превращается в число
Ещё один Suc: вставляем второй knownSuc. Остаётся найти словарь для Zero.
natValD (knownSuc (knownSuc□))
data KnownNatDict (n :: Nat) = KN { natValD :: Int }
knownZero = KN { natValD = 0 }
knownSuc d = KN { natValD = 1 + natValD d }
Тип словаря
Терм словаря
KnownNatDict (Suc (Suc Zero))
KnownNatDict (Suc Zero)
KnownNatDict Zero
Как словарь превращается в число
Для Zero есть готовый knownZero. Все дырки в терме заполнены; теперь собираем ответ снизу вверх.
natValD (knownSuc (knownSucknownZero))
data KnownNatDict (n :: Nat) = KN { natValD :: Int }
knownZero = KN { natValD = 0 }
knownSuc d = KN { natValD = 1 + natValD d }
Тип словаря
Терм словаря
KnownNatDict (Suc (Suc Zero))
KnownNatDict (Suc Zero)
KnownNatDict Zero
d0 = knownZero
Как словарь превращается в число
Внутренний словарь: d1 = knownSuc d0. Его поле задано выражением 1 + natValD d0.
natValD (knownSuc (knownSucknownZero))
data KnownNatDict (n :: Nat) = KN { natValD :: Int }
knownZero = KN { natValD = 0 }
knownSuc d = KN { natValD = 1 + natValD d }
Тип словаря
Терм словаря
KnownNatDict (Suc (Suc Zero))
KnownNatDict (Suc Zero)
d1 = knownSuc d0
KnownNatDict Zero
d0 = knownZero
Как словарь превращается в число
Внешний словарь: d2 = knownSuc d1. В вызов подставлен весь терм knownSuc (knownSuc knownZero).
natValD (knownSuc (knownSucknownZero))
data KnownNatDict (n :: Nat) = KN { natValD :: Int }
knownZero = KN { natValD = 0 }
knownSuc d = KN { natValD = 1 + natValD d }
Тип словаря
Терм словаря
KnownNatDict (Suc (Suc Zero))
d2 = knownSuc d1
KnownNatDict (Suc Zero)
d1 = knownSuc d0
KnownNatDict Zero
d0 = knownZero
Как словарь превращается в число
Теперь выполнение: natValD d2 выбирает поле внешнего словаря. Покажем содержимое каждого словаря.
natValDd2
data KnownNatDict (n :: Nat) = KN { natValD :: Int }
knownZero = KN { natValD = 0 }
knownSuc d = KN { natValD = 1 + natValD d }
Тип словаря
Словарь и его поле
KnownNatDict (Suc (Suc Zero))
d2 = KN { natValD = 1 + natValD d1 }
KnownNatDict (Suc Zero)
d1 = KN { natValD = 1 + natValD d0 }
KnownNatDict Zero
d0 = KN { natValD = 0 }
Как словарь превращается в число
Поле d2 — это 1 + natValD d1: одна единица и чтение поля следующего словаря.
1 +natValDd1
data KnownNatDict (n :: Nat) = KN { natValD :: Int }
knownZero = KN { natValD = 0 }
knownSuc d = KN { natValD = 1 + natValD d }
Тип словаря
Словарь и его поле
KnownNatDict (Suc (Suc Zero))
d2 = KN { natValD = 1 + natValD d1 }
KnownNatDict (Suc Zero)
d1 = KN { natValD = 1 + natValD d0 }
KnownNatDict Zero
d0 = KN { natValD = 0 }
Как словарь превращается в число
Поле d1 — ещё одна единица и natValD d0. Обе единицы получены из тела knownSuc.
1 + (1 +natValDd0)
data KnownNatDict (n :: Nat) = KN { natValD :: Int }
knownZero = KN { natValD = 0 }
knownSuc d = KN { natValD = 1 + natValD d }
Тип словаря
Словарь и его поле
KnownNatDict (Suc (Suc Zero))
d2 = KN { natValD = 1 + natValD d1 }
KnownNatDict (Suc Zero)
d1 = KN { natValD = 1 + natValD d0 }
KnownNatDict Zero
d0 = KN { natValD = 0 }
Как словарь превращается в число
Поле d0 равно 0: это тело knownZero. Осталось выполнить два сложения.
1 +(1 +0)
data KnownNatDict (n :: Nat) = KN { natValD :: Int }
knownZero = KN { natValD = 0 }
knownSuc d = KN { natValD = 1 + natValD d }
Тип словаря
Словарь и его поле
KnownNatDict (Suc (Suc Zero))
d2 = KN { natValD = 1 + natValD d1 }
KnownNatDict (Suc Zero)
d1 = KN { natValD = 1 + natValD d0 }
KnownNatDict Zero
d0 = KN { natValD = 0 }
Как словарь превращается в число
Внутреннее сложение даёт 1. Это значение поля natValD у словаря d1.
1 +1
data KnownNatDict (n :: Nat) = KN { natValD :: Int }
knownZero = KN { natValD = 0 }
knownSuc d = KN { natValD = 1 + natValD d }
Тип словаря
Словарь и его поле
KnownNatDict (Suc (Suc Zero))
d2 = KN { natValD = 1 + natValD d1 }
KnownNatDict (Suc Zero)
d1 = KN { natValD = 1 }
KnownNatDict Zero
d0 = KN { natValD = 0 }
Как словарь превращается в число
Внешнее сложение даёт 2. Это значение поля natValD у словаря d2 — и ответ исходного вызова.
2
data KnownNatDict (n :: Nat) = KN { natValD :: Int }
ghci> showReverse @'[] @'[Char, Int, Double]"Double Int Char"
Разворот списка типов по шагам
Каждый шаг вывода — выбор инстанса по голове. Подошёл второй: первый тип переезжает в аккумулятор.
Reverse '[] '[Char, Int, Double]
Разворот списка типов по шагам
Посылка второго инстанса — снова Reverse, с аккумулятором на один тип длиннее.
Reverse '[Char] '[Int, Double]
Разворот списка типов по шагам
Ещё шаг.
Reverse '[Int, Char] '[Double]
Разворот списка типов по шагам
Список опустел — теперь подходит первый инстанс.
Reverse '[Double, Int, Char] '[]
Разворот списка типов по шагам
Его посылка — ShowT для аккумулятора. При компиляции построен терм со словарями; при исполнении его методы строят строку.
ShowT '[Double, Int, Char]
Классы типов и вывод инстансов — кодогенерация
Мы пишем термы; по ним компилятор выводит типы.
термы, которые пишем мы
типы
вывод типов
Классы типов и вывод инстансов — кодогенерация
А по типам — инстансы, то есть снова термы. Интересный код пишем мы, неинтересный параллельно выписывает компилятор.
термы, которые пишем мы
типы
термы, которые пишет компилятор
вывод типов
вывод инстансов
Из значения — в тип
reify ::Int-> (forall n.KnownNat n => a) -> a -- тип вернуть нельзя: принимаем продолжениеreify n k| n <=0= k @Zero-- передаётся knownZero|otherwise= reify (n -1) \ @n' -> k @(Suc n') -- передаётся knownSuc d-- d — словарь KnownNat n'
wonderId ::Int->IntwonderId n = reify n (\ @t -> natVal @t) -- значение → тип → значение
reify 2: число убывает, словарь растёт
Покажем словарный перевод reify как reifyD: продолжение получает типовой аргумент и явный словарь.
reifyD2k
k :: ∀n. KnownNatDict n → a
Тип словаря
Словарь, передаваемый продолжению
reify 2: число убывает, словарь растёт
Число уменьшается до 1. Новое продолжение k₁ получит словарь d и передаст в k словарь knownSuc d.
reifyD1k₁
k :: ∀n. KnownNatDict n → a
k₁ = Λn. λd. k @(Suc n) (knownSuc d)
Тип словаря
Словарь, передаваемый продолжению
reify 2: число убывает, словарь растёт
Число уменьшается до 0. Продолжение k₂ добавит ещё один knownSuc, когда получит свой словарь.
reifyD0k₂
k :: ∀n. KnownNatDict n → a
k₁ = Λn. λd. k @(Suc n) (knownSuc d)
k₂ = Λn. λd. k₁ @(Suc n) (knownSuc d)
Тип словаря
Словарь, передаваемый продолжению
reify 2: число убывает, словарь растёт
База рекурсии: выбираем Zero, строим d0 = knownZero и передаём его продолжению k₂.
k₂@Zerod0
k :: ∀n. KnownNatDict n → a
k₁ = Λn. λd. k @(Suc n) (knownSuc d)
k₂ = Λn. λd. k₁ @(Suc n) (knownSuc d)
Тип словаря
Словарь, передаваемый продолжению
KnownNatDict Zero
d0 = knownZero
reify 2: число убывает, словарь растёт
Вызываем k₂: из d0 строится d1 = knownSuc d0. Продолжение k₁ получает тип Suc Zero и этот словарь.
k₁@(Suc Zero)d1
k :: ∀n. KnownNatDict n → a
k₁ = Λn. λd. k @(Suc n) (knownSuc d)
k₂ = Λn. λd. k₁ @(Suc n) (knownSuc d)
Тип словаря
Словарь, передаваемый продолжению
KnownNatDict Zero
d0 = knownZero
KnownNatDict (Suc Zero)
d1 = knownSuc d0
reify 2: число убывает, словарь растёт
Вызываем k₁: строится d2 = knownSuc d1. Исходное k получает тип Suc (Suc Zero) и готовый словарь.
k@(Suc (Suc Zero))d2
k :: ∀n. KnownNatDict n → a
k₁ = Λn. λd. k @(Suc n) (knownSuc d)
k₂ = Λn. λd. k₁ @(Suc n) (knownSuc d)
Тип словаря
Словарь, передаваемый продолжению
KnownNatDict Zero
d0 = knownZero
KnownNatDict (Suc Zero)
d1 = knownSuc d0
KnownNatDict (Suc (Suc Zero))
d2 = knownSuc (knownSuc knownZero)
Задача: удивительная тождественная функция
Что вернёт wonderId (-3)? Является ли wonderId тождественной функцией?
ghci> wonderId 55ghci> wonderId (-3)0
Для n <= 0 выбирается тип Zero: функция тождественна только на неотрицательных числах.
showAll(List(1,2,3))// "1, 2, 3": компилятор подставляет intShowshowAll(List(1,2,3))(intShow)// "1, 2, 3": передаём словарь самиimplicitly[Show[Int]].show(42)// "42": получаем словарь и вызываем его метод
Когерентность: два пути — один словарь
Нужен словарь Eq [a], в контексте есть Ord a. Первый путь: порядок на списках, а из него равенство.
Ord a
Ord [a]
Eq a
Eq [a]
instance Ord a => Ord [a]
class Eq a => Ord a
class Eq [a] => Ord [a]
instance Eq a => Eq [a]
Когерентность: два пути — один словарь
Второй путь: равенство на элементах, а из него равенство на списках.
Ord a
Ord [a]
Eq a
Eq [a]
instance Ord a => Ord [a]
class Eq a => Ord a
class Eq [a] => Ord [a]
instance Eq a => Eq [a]
Когерентность: два пути — один словарь
Когерентность: словари, полученные разными путями, неотличимы — диаграмма коммутирует. Откуда пришёл инстанс, можно не думать.
Ord a
Ord [a]
Eq a
Eq [a]
instance Ord a => Ord [a]
class Eq a => Ord a
class Eq [a] => Ord [a]
instance Eq a => Eq [a]
Задача: множество с двумя порядками
Множество Set a хранит элементы упорядоченными и при вставке и поиске пользуется порядком Ord a. Что произойдёт, если множество построить с одним порядком на элементах, а искать в нём с другим? Почему обычная когерентная модель классов типов в Haskell исключает такую ситуацию, а выбор имплисита из локального контекста допускает её?
Поиск свернёт не в то поддерево: элемент в множестве есть, а ответ — «нет». В обычной когерентной модели Haskell insert и member получают неотличимые словари Ord a. Имплисит берётся из области видимости места вызова — в двух местах реализации могут оказаться разными.
Правила переписывания и специализация
{-# RULES "map/map" forall f g xs. map f (map g xs) = map (f . g) xs -- закон функтора "map/append" forall f xs ys. map f (xs ++ ys) = map f xs ++ map f ys #-}
genericLookup ::Ord a =>Table a b -> a -> bintLookup ::TableInt b ->Int-> b{-# RULES "genericLookup/Int" genericLookup = intLookup #-}
Дефункционализация: лямбды становятся данными
У каждой лямбды есть тело; вторая ещё захватывает y из места вызова. Найдём коду и данным новое место.
map :: (Int → Int) → [Int] → [Int]
Места вызова: здесь остаются данные
apply: сюда переезжает код
example1 xs = map
example2 y xs = map
xs
xs
Захвачено:
λx.
x + 1
λx.
x * y
y
data Fun = …
Вызов внутри map
map f (x:xs) =
f x
: map f xs
Дефункционализация: лямбды становятся данными
Тело x + 1 переезжает в ветку apply F1 x. На месте первой лямбды остаётся конструктор F1.
map :: (Int → Int) → [Int] → [Int]
Места вызова: здесь остаются данные
apply: сюда переезжает код
example1 xs = map
example2 y xs = map
xs
xs
apply
F1
=
F1
Захвачено:
x
x + 1
λx.
x * y
y
data Fun = F1 | …
Вызов внутри map
map f (x:xs) =
f x
: map f xs
Дефункционализация: лямбды становятся данными
Тело x * y переезжает в ветку apply (F2 y) x. Захваченное y становится полем конструктора F2.
map :: (Int → Int) → [Int] → [Int]
Места вызова: здесь остаются данные
apply: сюда переезжает код
example1 xs = map
example2 y xs = map
xs
xs
apply
F1
=
F1
apply
(F2 y)
=
F2
F2 хранит y; apply извлекает его
x
x + 1
x
x * y
y
data Fun = F1 | F2 Int
Вызов внутри map
map f (x:xs) =
f x
: map f xs
Дефункционализация: лямбды становятся данными
map теперь получает данные типа Fun. Вместо вызова f x он вызывает интерпретатор: apply f x.
Добавим третий колсайт, в котором функция приходит снаружи: example3 f xs = map (\x -> f x + 1) xs. Как его дефункционализировать? Что для этого нужно знать об остальной программе?
dataFun=F1|F2Int|F3Fun-- f уже дефункционализирована: она тоже Funapply (F3 f) x = apply f x +1
Нужно знать все места, где создаются функции этого типа: преобразование применяется к программе целиком.
Конструктор типа становится символом
В Kotlin параметр типа не может быть конструктором типов: M<T> записать нельзя.
interface Monad<M> { fun <T> pure(x: T): M<T> }
Конструктор типа становится символом
Дефункционализация на уровне типов: конструктору — тип-символ, применению — тип Apply.
interface Monad<M> { fun <T> pure(x: T): Apply<M, T> }
List<T> ≅ Apply<ListSym, T>
Монада на Kotlin
class ListSym // символ конструктора Listclass Apply<Sym, T>(valvalue:Any)// «Sym, применённый к T»: тип значения стёртfun<T> List<T>.to():Apply<ListSym, T> = Apply(this)fun<T>Apply<ListSym, T>.from():List<T>=this.value as List<T>
classContainer c wheretypeElem c -- тип, который класс ставит в соответствие c elements :: c -> [Elem c]instanceContainer [a] wheretypeElem [a] = a elements =idinstanceContainerByteStringwheretypeElemByteString=Word8 elements =ByteString.unpack
ghci>:k!ElemByteString=Word8
Можно ли пройти назад
Конструктор типа пассивен: разные аргументы дают разные типы. Из Maybe a ~ Maybe b следует a ~ b.
Int
Char
Maybe Int
Maybe Char
Maybe
Maybe
Можно ли пройти назад
Семейство вычисляется, и разные аргументы могут дать один результат. По результату аргумент не восстановить.
Int
Char
Double
NonInjective
NonInjective
Int или Char?
Инъективность объявляют явно
f ::NonInjective a ->Stringerror:Couldn't match type:NonInjective a0 with:NonInjective aNB: ‘NonInjective’ is a non-injective typefamilyThetype variable ‘a0’ is ambiguous
typefamilyInj a = r | r -> a where-- TypeFamilyDependencies: результат определяет аргументInjInt=BoolInjChar=Double
Семейство нельзя передать аргументом
typefamilyMap (f ::Type->Type) (as :: [Type]) :: [Type] whereMap f '[] = '[]Map f (a : as) = f a :Map f astypefamilyId (a ::Type) ::TypewhereId a = atypeTest=MapId '[Int, Bool]error:Thetypefamily ‘Id’ should have 1 argument, but has been given none
ΔMatchability — инъективность вместе с generativity: из f a ~ g a следует f ~ g. Вывод типов предполагает её у всякого типа стрелочного кайнда
Обход: снова дефункционализация
typefamilyApply (sym ::Type) (a ::Type) ::Type-- открытое семейство-интерпретаторdataIdSym-- символ вместо семейства IdtypeinstanceApplyIdSym a = atypefamilyMap (f ::Type) (as :: [Type]) :: [Type] whereMap f '[] = '[]Map f (a : as) =Apply f a :Map f as
ghci>:k!MapIdSym '[Int, Bool]= [Int, Bool]
Кайнд Constraint
У ограничений есть кайнд
ghci>:k MonadMonad :: (Type->Type) ->Constraint-- класс типов строит ограничениеghci>:k Int~BoolInt~Bool ::Constraint-- равенство типов — ограничениеghci>:k (EqInt, Int~Bool)(EqInt, Int~Bool) ::Constraint-- кортеж ограничений — ограничение
dataDict (c ::Constraint) where-- словарь как объект языкаDict :: c =>Dict c
Ограничение на каждый тип из списка
dataHList (tys :: [Type]) where-- гетерогенный список из главы 2HNil ::HList '[]HCons :: ty ->HList tys ->HList (ty : tys)
typefamilyAll (c :: k ->Constraint) (tys :: [k]) ::ConstraintwhereAll c '[] = ()All c (ty : tys) = (c ty, All c tys)
All: семейство строит ограничение
Семейство возвращает не тип, а ограничение. Второе уравнение отщепляет первый тип списка.
All Show '[Int, Double]
typefamilyAll (c :: k ->Constraint) (tys :: [k]) ::ConstraintwhereAll c '[] = ()All c (ty : tys) = (c ty, All c tys)
All: семейство строит ограничение
Ещё раз второе уравнение.
(Show Int,All Show '[Double])
typefamilyAll (c :: k ->Constraint) (tys :: [k]) ::ConstraintwhereAll c '[] = ()All c (ty : tys) = (c ty, All c tys)
All: семейство строит ограничение
Список пуст — первое уравнение, пустое ограничение.
(Show Int,(Show Double,All Show '[]))
typefamilyAll (c :: k ->Constraint) (tys :: [k]) ::ConstraintwhereAll c '[] = ()All c (ty : tys) = (c ty, All c tys)
All: семейство строит ограничение
Получился вложенный кортеж ограничений — по одному на элемент списка.
(Show Int,(Show Double,()))
typefamilyAll (c :: k ->Constraint) (tys :: [k]) ::ConstraintwhereAll c '[] = ()All c (ty : tys) = (c ty, All c tys)
map по гетерогенному списку
hmap ::forall c res tys .All c tys=> (forall ty . c ty => ty -> res) ->HList tys -> [res]hmap f = \caseHNil-> []HCons x xs -> f x : hmap @c f xs
Реализуйте сравнение гетерогенных списков heq :: All Eq tys => HList tys -> HList tys -> Bool. Откуда в рекурсивном вызове берётся словарь для хвоста списка?
heq ::AllEq tys =>HList tys ->HList tys ->Boolheq HNilHNil=Trueheq (HCons x xs) (HCons y ys) = x == y && heq xs ys
Сопоставление с HCons даёт tys ~ ty : tys', и All Eq tys вычисляется в пару (Eq ty, All Eq tys'): первая компонента нужна для ==, вторая — словарь для хвоста.
Ограничения с кванторами
dataRose f x =Rose x (f (Rose f x))instance (Eq a, forall b.Eq b =>Eq (f b)) =>Eq (Rose f a) where-- QuantifiedConstraintsRose x1 rs1 ==Rose x2 rs2 = x1 == x2 && rs1 == rs2
Использование ad-hoc полиморфизма
Сериализатор пары — это listOrd
interface KSerializer<T>{funserialize(encoder:Encoder,value:T)fundeserialize(decoder:Decoder):T// объекта ещё нет: методом объекта не сделать}classPairSerializer<K, V>(keySerializer:KSerializer<K>,// словари компонент передаются рукамиvalueSerializer:KSerializer<V>,):KSerializer<Pair<K, V>>{...}
Спрятать тип, оставить словарь
Конструктор Any :: a -> Any прячет тип значения. В один список попадают значения разных типов — но сделать с ними нельзя ничего.
list :: [Any]
Any •
Any •
Any •
42
“Hello”
True
тип неизвестен
тип неизвестен
тип неизвестен
Спрятать тип, оставить словарь
Положим рядом со значением словарь — свидетельство того, что скрытый тип принадлежит классу.
list :: [Has Show]
Has • •
Has • •
Has • •
42
“Hello”
True
show для Int
show для String
show для Bool
Экзистенциальный тип с ограничением
dataHas (c ::Type->Constraint) whereHas :: c a => a ->Has c -- тип a спрятан, словарь c a сохранёнshowAll :: [HasShow] ->StringshowAll = List.intercalate ",".map \(Has x) ->show x
ghci> showAll [Has (42 ::Int), Has"Hello", Has (JustTrue)]"42,\"Hello\",Just True"
foldHas ::Has c -> (forall a . c a => a -> b) -> b -- общий способ разобрать HasfoldHas (Has x) k = k x
Задача: достать значение из обёртки
Почему нельзя написать функцию unHas :: Has c -> a, достающую значение из обёртки? Чем от неё отличается foldHas?
unHas (Has x) = xerror:Couldnot deduce ‘a1 ~ a’ from the context: c a1 ‘a1’ is a rigid type variable bound by a pattern with constructor:Has
В unHas тип a выбирает вызывающий, а в обёртке лежит значение типа, выбранного при её создании. В foldHas квантор стоит слева от стрелки: продолжение обязано принять любой тип, и скрытый тип не покидает своей области — как s в runST.
Имя поля выбирается по типу
classIsLabel (s ::Symbol) a where fromLabel :: a -- OverloadedLabels: #name ≡ fromLabel @"name"dataPet=Pet { name ::String }instance (r ~String) =>IsLabel"name" (Pet-> r) where fromLabel Pet{ name } = namedataPerson=Person { name ::String, pets :: [Pet] }instance (r ~String) =>IsLabel"name" (Person-> r) where fromLabel Person{ name } = name
ghci>#name pet"Rex"
Constraint trick: сначала голова, потом посылки
Инстанс для Pet -> String. Тип результата #name pet пока неизвестен, а цель сравнивается только с головой инстанса: не совпало.
цель: IsLabel "name" (Pet →r₀)
голова: IsLabel "name" (Pet →String)
Constraint trick: сначала голова, потом посылки
Оставим голову общей — она подойдёт к любой цели. Равенство стоит в посылке и сработает уже после выбора инстанса.
цель: IsLabel "name" (Pet →r₀)
голова: IsLabel "name" (Pet →r)
посылка: r ~ String
Равенство типов — тоже ограничение
f ::forall a b . a ~ b => a -> b -- четыре параметра: два типа, коерция и аргументf =id
ΔКоерция — свидетель равенства двух типов: значение размера 0, которое строит компилятор
ΔSystem FC — System F с несинтаксическими равенствами типов: маленький типизированный язык, в который транслируется весь Haskell
GADT — это равенства типов
Конструкторы GADT из главы 2: у каждого свой тип результата.
Const ::Int → ExprInt
IsZero ::Expr Int → ExprBool
GADT — это равенства типов
В System FC результат у всех один, а уточнение записано равенством. Сопоставление с образцом вносит это равенство в ветку.
Const :: ∀ty. ty ~ Int ⇒ Int → Expr ty
IsZero :: ∀ty. ty ~ Bool ⇒ Expr Int → Expr ty
Семейства — это аксиомы
typefamilyPlus (n ::Nat) (m ::Nat) ::NatwherePlusZero m = mPlus (Suc n) m =Suc (Plus n m)
axiom PlusZero m ~ m -- порождает компиляторaxiom Plus (Suc n) m ~Suc (Plus n m)
Задача: вектор в равенствах
Рассахарьте в равенства типов вектор Vec из главы 2. Какие равенства окажутся в области видимости в каждой из веток функции vzip?
dataVec n a whereVNil ::forall n a . n ~Zero=>Vec n aVCons ::forall n a m . n ~Suc m => a ->Vec m a ->Vec n a
В первой ветке vzip — n ~ Zero. Во второй — n ~ Suc m от первого аргумента и n ~ Suc m' от второго; отсюда m ~ m', потому что Suc — конструктор, и рекурсивный вызов типизируется.
Обход, который ничего не делает
newtypeCsv=Csv { unCsv ::String }concatC :: [Csv] ->CsvconcatC =Csv.concat.map unCsv -- map проходит по списку и ничего не меняет
concatC :: [Csv] ->CsvconcatC = coerce (concat :: [String] ->String)classCoercible from to where-- упрощённо; инстансы строит компилятор coerce :: from -> to
coerce меняет тип, а не память
Список обёрток в куче. Конструктора Csv во время исполнения нет.
регистры и стек
куча
xs :: [Csv]
•
(:) │ • │ •
(:) │ • │ •
[]
“a,b”
“;c”
coerce меняет тип, а не память
После coerce изменился только тип. Ни одна ячейка не тронута, обхода нет.
регистры и стек
куча
xs :: [String]
•
(:) │ • │ •
(:) │ • │ •
[]
“a,b”
“;c”
Роли: когда приводить можно
Роль параметра
Пример
T a приводится к T b, если
phantom
data Phantom a = Phantom
всегда
representational
data Box a = Box a
a приводится к b
nominal
data App a = App (F a)
a ~ b
{-# LANGUAGE RoleAnnotations #-}type role Map nominal representational -- по одной роли на параметр: k, vdataMap k v =Tip|Bin k v (Map k v) (Map k v) -- упрощённое дерево поиска
ghci> coerce (Just (ModuleId1)) ::MaybeCourseIdJust (CourseId1)ghci> coerce m ::Map (DownInt) String-- m :: Map Int Stringerror:Couldn't match type ‘Int’ with ‘DownInt’ arising from a use of ‘coerce’
Задача: роль ключа в Map
Почему у первого параметра Map k v роль nominal? Приведите пример обёртки над ключом, для которой приведение Map k v к Map k' v сломало бы словарь.
Дерево поиска упорядочено по инстансу Ord k, а он зависит от имени типа, а не от представления. Down Int устроен в памяти как Int, но порядок у него обратный: после приведения дерево осталось бы прежним, а поиск пошёл бы по нему с другим порядком — как в задаче про множество с двумя порядками.
dataDynamicwhereDynamic ::Typeable a => a ->Dynamic-- экзистенциальный тип со словарём Typeableghci> d = toDyn (42 ::Int)ghci> fromDynamic d ::MaybeIntJust42ghci> fromDynamic d ::MaybeBoolNothing
Задача: список типов в строку
Реализуйте класс ShowT (tys :: [Type]) с методом showTypes :: String, который позволяет распечатать список типов.
Исключение и его предки
Ошибки обрабатывают группами — нужна иерархия. Наследования в языке нет, а иерархия должна быть открытой: новый тип добавляется в любом модуле.
SomeException
SomeArithException
MyError
DivisionByZero
Overflow
Исключение и его предки
throw DivisionByZero: метод toException заворачивает значение в обёртки всех предков.
Обработчик для MyError: обёртка снята, но внутри значение другого типа, cast не удался. Исключение летит дальше.
SomeException
SomeArithException
DivisionByZero
cast к MyError: Nothing
Класс Exception
class (Typeable a, Show a) =>Exception a where toException :: a ->SomeException toException =SomeException-- по умолчанию родитель — корень fromException ::SomeException->Maybe a fromException (SomeException e) = cast e
dataSomeExceptionwhereSomeException ::Exception a => a ->SomeException-- экзистенциальная обёртка, как Dynamic
Промежуточный уровень иерархии
dataSomeArithExceptionwhereSomeArithException ::Exception a => a ->SomeArithExceptioninstanceExceptionSomeArithException-- родитель — SomeExceptiondataDivisionByZero=DivisionByZeroderivingShowinstanceExceptionDivisionByZerowhere toException = toException .SomeArithException-- заворачиваемся в родителя fromException e =doSomeArithException e' <- fromException e -- снимаем обёртку родителя cast e'
Добавьте в иерархию исключение Overflow, тоже арифметическое. Какими обработчиками оно ловится, а какими нет?
dataOverflow=OverflowderivingShowinstanceExceptionOverflowwhere toException = toException .SomeArithException fromException e =doSomeArithException e' <- fromException e cast e'
Ловится обработчиками для Overflow, SomeArithException и SomeException; обработчики для DivisionByZero и MyError его пропустят.
Стек вызовов — неявный параметр
myHead ::HasCallStack=> [a] -> amyHead [] =error"empty"myHead (x:_) = xbad ::Intbad = myHead [] -- у bad ограничения нет: выше трейс не идёт
ghci> bad***Exception: emptyCallStack (from HasCallStack):error, called at Bad.hs:5:17in main:Main myHead, called at Bad.hs:9:7in main:Main
typeHasCallStack= (?callStack ::CallStack)
Свои сообщения об ошибках типов
instance (TypeError ( Text"Attempting to show a function of type ":<>:Text"'":<>:ShowType (a -> b) :<>:Text"'":$$:Text"Did you forget to apply an argument?" )) =>Show (a -> b) whereshow=undefined-- до исполнения дело не дойдёт
test =shownoterror:Attempting to show a function oftype'Bool->Bool'Did you forget to apply an argument?
Пока значение никому не понадобилось, в куче лежат одни отложенные вычисления.
(:) │ • │ •
(:) │ • │ •
(:) │ • │ •
[]
1
undefined
3
seq, length и deepseq
seq вычисляет до первого конструктора — до слабой головной нормальной формы. Поля не тронуты.
(:) │ • │ •
(:) │ • │ •
(:) │ • │ •
[]
1
undefined
3
seq, length и deepseq
length идёт по хвостам: вычислен весь хребет списка, элементы остались отложенными.
(:) │ • │ •
(:) │ • │ •
(:) │ • │ •
[]
1
undefined
3
seq, length и deepseq
deepseq обходит значение целиком, до нормальной формы, — и натыкается на undefined.
(:) │ • │ •
(:) │ • │ •
(:) │ • │ •
[]
1
undefined
3
Обход зависит от типа — значит, класс
classNFData a where rnf :: a -> () -- reduce to normal forminstanceNFDataIntwhere rnf x = x `seq` ()instanceNFData a =>NFData [a] where rnf [] = () rnf (x : xs) = rnf x `seq` rnf xsforce ::NFData a => a -> aforce x = rnf x `seq` x
safe, safe' :: [Int] ->IO (EitherSomeException [Int])safe xs = try (evaluate xs) -- не поймает: вычислен только первый конструкторsafe' xs = try (evaluate (force xs)) -- поймает
− Цена: rnf обходит значение целиком при каждом вызове, даже если оно уже вычислено
Задачи: нормальная форма
Реализуйте инстанс NFData для дерева data Tree a = Leaf | Node (Tree a) a (Tree a).
Почему length xs `seq` () не заменяет rnf xs? Что именно вычислит каждое из выражений?
instanceNFData a =>NFData (Tree a) where rnf Leaf= () rnf (Node l x r) = rnf l `seq` rnf x `seq` rnf r
length xs вычисляет хребет списка и оставляет элементы отложенными; rnf xs вычисляет и хребет, и каждый элемент.
Что мы узнали
классы типов
словарь, который компилятор передаёт неявно; инстанс с посылкой — функция между словарями
вывод инстансов
население типа рекурсией по его структуре; смотрит только на голову инстанса
дефункционализация
функция первого класса = данные + интерпретатор apply; работает и на типах
семейства
выбор по типу для данных и типов; не инъективны и не передаются аргументами
равенства типов: во что транслируются GADT, семейства и newtype
Домашка fp2-03: словари и инстансы, дефункционализация, семейства типов
Следующая тема: типы данных — глава 4
Материалы
Факультативно: data reflection
classReifies ty terms | ty -> terms where reflect ::Proxy ty -> termsreify :: a -> (forall fresh .Reifies fresh a =>Proxy fresh -> res) -> res
newtypeWrapped tag a =Wrapped { unwrap :: a } -- фантомный тег: сколько угодно обёртокdataReifiedOrd a =ReifiedOrd { compare :: a -> a ->Ordering }instanceReifies tag (ReifiedOrd a) =>Ord (Wrapped tag a) wherecompare= coerce $compare$ reflect $Proxy@tag
Факультативно: локальный порядок для сортировки
sort ::Ord a => [a] -> [a]sortReverse ::forall a .Ord a => [a] -> [a]sortReverse xs =let dict =ReifiedOrd { compare=flipcompare } in reify dict \(Proxy ::Proxy fresh) -> coerce $sort@(Wrapped fresh a) $ coerce xs
(Int, Double) заменяем на (MemberInt d, MemberDouble d) =>Prod d -- открытые структуры
Что почитать
Конспект курса, глава 3 «Специальный (ad-hoc) полиморфизм»