Есть ли хороший ресурс для реального использования обобщенного алгебраического типа данных?
Ответ 2
GADT - слабые аппроксимации индуктивных семейств из языков, типично типизированных, поэтому начнем там.
Индуктивные семейства - это метод ввода базового типа данных на языке с навязчивым языком. Например, в Agda вы определяете натуральные числа, подобные этому
data Nat : Set where
zero : Nat
succ : Nat -> Nat
что не очень причудливо, по существу это то же самое, что определение Haskell
data Nat = Zero | Succ Nat
и действительно в синтаксисе GADT форма Haskell еще более похожа на
{-# LANGUAGE GADTs #-}
data Nat where
Zero :: Nat
Succ :: Nat -> Nat
Итак, сначала краснеть, вы можете подумать, что GADT - просто аккуратный дополнительный синтаксис. Это только самый верхушка айсберга.
Agda имеет способность представлять все типы типов, незнакомые и незнакомые программисту Haskell. Простым является тип конечных множеств. Этот тип написан как Fin 3
и представляет собой набор чисел {0, 1, 2}
. Аналогично, Fin 5
представляет собой набор чисел {0,1,2,3,4}
.
Это должно быть довольно странно на данный момент. Во-первых, мы имеем в виду тип, который имеет регулярное число как параметр "type". Во-вторых, неясно, что означает Fin n
для представления набора {0,1...n}
. В реальной Агда мы сделали бы что-то более мощное, но достаточно сказать, что мы можем определить функцию contains
contains : Nat -> Fin n -> Bool
contains i f = ?
Теперь это снова странно, потому что "естественное" определение contains
будет чем-то вроде i < n
, но n
- это значение, которое существует только в типе Fin n
, и мы не можем крест, который так легко делит. В то время как выясняется, что определение не так прямолинейно, это именно та сила, которую имеют индуктивные семейства на языках с навязчивым языком - они вводят значения, зависящие от их типов и типов, которые зависят от их значений.
Мы можем проверить, что это значит о Fin
, который дает это свойство, просматривая его определение.
data Fin : Nat -> Set where
zerof : (n : Nat) -> Fin (succ n)
succf : (n : Nat) -> (i : Fin n) -> Fin (succ n)
это требует немного работы, чтобы понять, так что пример позволяет попытаться построить значение типа Fin 2
. Есть несколько способов сделать это (на самом деле мы обнаружим, что есть ровно 2)
zerof 1 : Fin 2
zerof 2 : Fin 3 -- nope!
zerof 0 : Fin 1 -- nope!
succf 1 (zerof 0) : Fin 2
Это позволяет нам видеть, что есть два обитателя, а также немного показывает, как происходит вычисление типов. В частности, бит (n : Nat)
в типе zerof
отражает фактическое значение n
в тип, позволяющий нам сформировать Fin (n+1)
для любого n : Nat
. После этого мы используем повторные приложения succf
, чтобы увеличить наши значения Fin
до правильного индекса семейства типов (натуральное число, которое индексирует Fin
).
Что обеспечивает эти способности? Честно говоря, существует много различий между зависимой типизированной индуктивной семьей и регулярным Haskell ADT, но мы можем сосредоточиться на том, что наиболее важно для понимания GADT.
В GADT и индуктивных семействах вы получаете возможность указать точный тип ваших конструкторов. Это может быть скучно
data Nat where
Zero :: Nat
Succ :: Nat -> Nat
Или, если у нас есть более гибкий, индексированный тип, мы можем выбрать разные, более интересные типы возврата
data Typed t where
TyInt :: Int -> Typed Int
TyChar :: Char -> Typed Char
TyUnit :: Typed ()
TyProd :: Typed a -> Typed b -> Typed (a, b)
...
В частности, мы злоупотребляем возможностью изменять тип возврата на основе используемого конструктора значений. Это позволяет нам отображать некоторую информацию о значении в тип и производить более тонко заданные (с волоконным) типом.
Итак, что мы можем с ними делать? Ну, с небольшим количеством смазки локтя, мы можем произвести Fin
в Haskell. Вкратце это требует, чтобы мы определяли понятие натуральности в типах
data Z
data S a = S a
> undefined :: S (S (S Z)) -- 3
... затем GADT, чтобы отражать значения в этих типах...
data Nat where
Zero :: Nat Z
Succ :: Nat n -> Nat (S n)
... тогда мы можем использовать их для построения Fin
, как это было в Agda...
data Fin n where
ZeroF :: Nat n -> Fin (S n)
SuccF :: Nat n -> Fin n -> Fin (S n)
И, наконец, мы можем построить ровно два значения Fin (S (S Z))
*Fin> :t ZeroF (Succ Zero)
ZeroF (Succ Zero) :: Fin (S (S Z))
*Fin> :t SuccF (Succ Zero) (ZeroF Zero)
SuccF (Succ Zero) (ZeroF Zero) :: Fin (S (S Z))
Но обратите внимание, что мы потеряли много удобства для индуктивных семей. Например, мы не можем использовать обычные числовые литералы в наших типах (хотя это технически всего лишь трюк в Agda), нам нужно создать отдельный "тип nat" и "value nat" и использовать GADT, чтобы связать их вместе, и со временем мы также найдем, что, хотя математика типа уровня является болезненной в Агда, это может быть сделано. В Haskell это невероятно болезненно и часто не может.
Например, можно определить понятие weaken
в Agda Fin
type
weaken : (n <= m) -> Fin n -> Fin m
weaken = ...
где мы предоставляем очень интересное первое значение, доказательство того, что n <= m
, которое позволяет вставлять "значение меньше n
" в набор "значений меньше m
". Мы можем сделать то же самое в Haskell, технически, но это требует серьезного злоупотребления прологом типа.
Таким образом, GADT представляют собой сходство индуктивных семейств в зависимых типизированных языках, которые слабее и неуклюже. Почему мы хотим их в Haskell в первую очередь?
В основном потому, что не все инварианты типа требуют полной мощности индуктивных семейств, и GADTs выбирают конкретный компромисс между выразительностью, реализацией в Haskell и типом вывода.
Некоторые примеры полезных выражений GADT: Красно-черные деревья, которые не могут иметь свойство Red-Black недействительным или простое типизированное лямбда-исчисление, встроенное в систему управления ходом HASK с системой Haskell.
На практике вы также часто видите, что GADT используют для своего неявного экзистенциального контекста. Например, тип
data Foo where
Bar :: a -> Foo
неявно скрывает переменную типа a
, используя экзистенциальную квантификацию
> :t Bar 4 :: Foo
что иногда удобно. Если вы внимательно посмотрите пример HOAS из Википедии, это используется для параметра типа a
в конструкторе App
. Чтобы выразить это утверждение без GADT, будет беспорядок экзистенциальных контекстов, но синтаксис GADT делает его естественным.