Реальное использование GADT - PullRequest
43 голосов
/ 05 октября 2010

Как использовать обобщенный алгебраический тип данных?

Пример, приведенный в haskell wikibook , слишком короткий, чтобы дать мне представление о реальных возможностях GADT.

Ответы [ 5 ]

52 голосов
/ 17 января 2014

GADT - это слабые аппроксимации индуктивных семейств из языков с зависимой типизацией - поэтому давайте вместо этого начнем.

Индуктивные семейства являются основным методом введения типов данных на языке с типизированной зависимостью. Например, в Agda вы определяете натуральные числа, как это

data Nat : Set where
  zero : Nat
  succ : Nat -> Nat 

что не очень необычно, по сути, это то же самое, что определение Хаскелла

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}.

Это должно быть довольно странно на данный момент. Во-первых, мы ссылаемся на тип, который имеет регулярное число в качестве параметра типа. Во-вторых, не ясно, что означает для 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).

Что дает эти способности? Честно говоря, существует много различий между индуктивным семейством с зависимой типизацией и обычным ADT на Haskell, но мы можем сосредоточиться на точном, наиболее подходящем для понимания 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 так же, как мы делали в Агде ...

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), нам нужно создать отдельные «type nat» и «value nat» и использовать GADT для их соединения, со временем мы также обнаружим, что хотя математика на уровне типов болезненна в Агде, это можно сделать. В Хаскеле это невероятно больно и часто не может.

Например, можно определить понятие weaken в типе Fin Агды

weaken : (n <= m) -> Fin n -> Fin m
weaken = ...

, где мы предоставляем очень интересное первое значение, доказательство того, что n <= m, который позволяет нам встраивать «значение меньше n» в набор «значений меньше m». Технически мы можем сделать то же самое в Haskell, но это требует серьезного злоупотребления прологом класса типов.


Таким образом, GADT - это сходство индуктивных семейств в языках с типизированной зависимостью, которые являются более слабыми и неуклюжими. Почему мы хотим их в Хаскеле в первую очередь?

В основном потому, что не все инварианты типов требуют полной мощности индуктивных семейств для выражения, а GADT выбирают определенный компромисс между выразительностью, реализуемостью в Haskell и выводом типов.

Некоторыми примерами полезных выражений GADT являются Красно-черные деревья, для которых свойство Красно-черного не может быть признано недействительным или лямбда-исчисление с простыми типами, встроенное в качестве HOAS-контекста в систему типов Haskell .

На практике вы также часто видите использование GADT для их неявного экзистенциального контекста. Например, тип

data Foo where
  Bar :: a -> Foo

неявно скрывает переменную типа a, используя экзистенциальную квантификацию

> :t Bar 4 :: Foo

способом, который иногда удобен. Если вы внимательно посмотрите, пример HOAS из Википедии использует это для параметра типа a в конструкторе App. Выражение этого утверждения без GADT было бы беспорядочным контекстом, но синтаксис GADT делает его естественным.

24 голосов
/ 27 апреля 2013

GADT могут дать вам более строгие гарантии, чем обычные ADT. Например, вы можете принудительно сбалансировать двоичное дерево на уровне системы типов, как в этой реализации из 2-3 деревьев :

{-# LANGUAGE GADTs #-}

data Zero
data Succ s = Succ s

data Node s a where
    Leaf2 :: a -> Node Zero a
    Leaf3 :: a -> a -> Node Zero a
    Node2 :: Node s a -> a -> Node s a -> Node (Succ s) a
    Node3 :: Node s a -> a -> Node s a -> a -> Node s a -> Node (Succ s) a

Каждый узел имеет кодированную глубину, где находятся все его листья. Дерево тогда либо пустое дерево, одноэлементное значение, либо узел неопределенной глубины, опять же используя ГАДЦ.

data BTree a where
    Root0 :: BTree a
    Root1 :: a -> BTree a
    RootN :: Node s a -> BTree a

Система типов гарантирует, что только сбалансированные узлы могут быть построены. Это означает, что при реализации таких операций, как insert на таких деревьях, ваш Проверяет тип кода только в том случае, если его результат всегда является сбалансированным деревом.

15 голосов
/ 05 октября 2010

Я нашел монаду «Подсказка» (из пакета «MonadPrompt») очень полезным инструментом в нескольких местах (вместе с эквивалентной монадой «Программа» из пакета «оперативный». В сочетании с GADT (как это предназначено для использования), оно позволяет создавать встроенные языки очень дешево и очень гибко. В выпуске Monad Reader 15 была довольно хорошая статья под названием «Приключения в трех монадах», в которой было хорошее введение. в монаду «Подсказка» вместе с некоторыми реалистичными ГАДЦами.

3 голосов
/ 08 октября 2010

Мне нравится пример в руководстве по GHC .Это короткая демонстрация основной идеи GADT: вы можете встроить систему типов языка, которым вы манипулируете, в систему типов Haskell.Это позволяет вашим функциям на Haskell предполагать и заставлять их сохранять, что деревья синтаксиса соответствуют хорошо типизированным программам.

Когда мы определяем Term, не имеет значения, какие типы мы выбираем.Мы могли бы написать

data Term a where
  ...
  IsZero :: Term Char -> Term Char

или

  ...
  IsZero :: Term a -> Term b

, и определение Term все равно будет выполнено.

Это только когда мы захотим вычислитьна Term, например, при определении eval, что типы имеют значение.Нам нужно иметь

  ...
  IsZero :: Term Int -> Term Bool

, потому что нам нужен наш рекурсивный вызов eval, чтобы вернуть Int, а мы, в свою очередь, хотим вернуть Bool.

2 голосов
/ 05 октября 2010

Это короткий ответ, но проконсультируйтесь с Haskell Wikibook.Он показывает GADT для хорошо типизированного дерева выражений, что является довольно каноническим примером: http://en.wikibooks.org/wiki/Haskell/GADT

GADT также используются для реализации равенства типов: http://hackage.haskell.org/package/type-equality. Я не могу найтиправильная статья, чтобы ссылаться на это безобразие - эта техника уже хорошо вошла в фольклор.Однако он используется довольно хорошо в набранных без тегов материалах Олега.Смотрите, например, раздел о типизированной компиляции в GADT.http://okmij.org/ftp/tagless-final/#tc-GADT

...