-
GADTSyntax -
Разрешает использовать синтаксис GADT в определениях типов данных (но не сами GADTs; для этого см.
GADTs)
Когда расширение GADTSyntax включено, GHC позволяет объявлять алгебраический тип данных, явно указывая сигнатуры типов конструкторов. Например:
data Maybe a where
Nothing :: Maybe a
Just :: a -> Maybe a
newtype Down a where
Down :: a -> Down a
Такая форма называется «объявлением в стиле GADT», потому что обобщённые алгебраические типы данных, описанные в Обобщённые алгебраические типы данных (GADTs), могут быть объявлены только с помощью этой формы.
Обратите внимание, что синтаксис в стиле GADT обобщает экзистенциальные типы (Экзистенциально квантованные конструкторы данных). Например, эти два объявления эквивалентны:
data Foo = forall a. MkFoo a (a -> Bool)
data Foo' where { MKFoo :: a -> (a->Bool) -> Foo' }
Любой тип данных (или новый тип), который может быть объявлен в стандартном синтаксисе Haskell 98, также может быть объявлен с использованием синтаксиса в стиле GADT. Выбор в основном стилистический, но объявления в стиле GADT отличаются в одном важном аспекте: они по-разному обрабатывают ограничения классов для конструкторов данных. В частности, если конструктор получает контекст класса типов, этот контекст становится доступным при сопоставлении с образцом. Например:
data Set a where
MkSet :: Eq a => [a] -> Set a
makeSet :: Eq a => [a] -> Set a
makeSet xs = MkSet (nub xs)
insert :: a -> Set a -> Set a
insert a (MkSet as) | a `elem` as = MkSet as
| otherwise = MkSet (a:as)
Использование MkSet как конструктора (например, в определении makeSet) приводит к ограничению (Eq a), как и ожидалось. Новая особенность заключается в том, что сопоставление с образцом на MkSet (как в определении insert) делает доступным контекст (Eq a). С точки зрения реализации, конструктор MkSet имеет скрытое поле, хранящее словарь (Eq a), который передаётся в MkSet; поэтому при сопоставлении с образцом этот словарь становится доступным для правой части сопоставления. В примере словарь равенства используется для удовлетворения ограничения равенства, сгенерированного вызовом elem, таким образом, тип insert сам по себе не имеет ограничения Eq.
Например, одно возможное применение заключается в реификационной словарей:
data NumInst a where MkNumInst :: Num a => NumInst a intInst :: NumInst Int intInst = MkNumInst plus :: NumInst a -> a -> a -> a plus MkNumInst p q = p + q
Здесь значение типа NumInst a эквивалентно явным словарю (Num a).
Всё это относится к конструкторам, объявленным с помощью синтаксиса Экзистенциальные объекты и классы типов. Например, тип данных NumInst выше можно эквивалентно объявить так:
data NumInst a = Num a => MkNumInst (NumInst a)
Обратите внимание, что, в отличие от ситуации при объявлении экзистенциального типа, здесь нет forall, поскольку Num ограничивает универсальную квантифицированную переменную типа a типа данных. Конструктор может иметь как универсальные, так и экзистенциальные переменные типа: например, следующие два объявления эквивалентны:
data T1 a = forall b. (Num a, Eq b) => MkT1 a b data T2 a where MkT2 :: (Num a, Eq b) => a -> b -> T2 a
Всё это поведение отличается от специфичного обращения Haskell 98 с контекстами в объявлении типа данных (раздел 4.2.1 отчета Haskell 98). В Haskell 98 определение
data Eq a => Set' a = MkSet' [a]
присваивает MkSet' тот же тип, что и MkSet выше. Но вместо того, чтобы делать доступным ограничение (Eq a), сопоставление с образцом на MkSet' требует ограничения (Eq a)! GHC исправно реализует это поведение, хоть оно и странное. Но для объявлений в стиле GADT поведение GHC гораздо более полезно и интуитивно понятно.
Обратите внимание, что ограничения Ограничения всё ещё действуют; например, новый тип, объявленный с помощью GADTSyntax не может использовать экзистенциальное квантование.
6.4.7.1. Формальный синтаксис для GADTs
Чтобы точнее определить, что разрешается, а что нет внутри конструктора в стиле GADT, мы предоставляем грамматику в стиле БНФ для GADT ниже. Обратите внимание, что эта грамматика может быть изменена в будущем.
gadt_con ::= conids '::' opt_forall opt_ctxt gadt_body
conids ::= conid
| conid ',' conids
opt_forall ::= <empty>
| 'forall' tv_bndrs '.'
tv_bndrs ::= <empty>
| tv_bndr tv_bndrs
tv_bndr ::= tyvar
| '(' tyvar '::' ctype ')'
opt_ctxt ::= <empty>
| btype '=>'
| '(' ctxt ')' '=>'
ctxt ::= ctype
| ctype ',' ctxt
gadt_body ::= prefix_gadt_body
| record_gadt_body
prefix_gadt_body ::= '(' prefix_gadt_body ')'
| return_type
| opt_unpack btype '->' prefix_gadt_body
record_gadt_body ::= '{' fieldtypes '}' '->' return_type
fieldtypes ::= <empty>
| fieldnames '::' opt_unpack ctype
| fieldnames '::' opt_unpack ctype ',' fieldtypes
fieldnames ::= fieldname
| fieldname ',' fieldnames
opt_unpack ::= opt_bang
: {-# UNPACK #-} opt_bang
| {-# NOUNPACK #-} opt_bang
opt_bang ::= <empty>
| '!'
| '~'
Где:
-
btype— это тип, которому не разрешено иметь внешнийforall/=>, если он не окружён скобками. Например,forall a. aиEq a => aне являются допустимымиbtypeами, но(forall a. a)и(Eq a => a)являются допустимыми. -
ctype— этоbtype, которому не накладываются ограничения на внешнийforall/=>, поэтомуforall a. aиEq a => aявляются допустимымиctypeами. -
return_type— это тип, которому не разрешено содержатьforallы,=>ы или->ы.
Это упрощённая грамматика, которая не полностью раскрывает все детали реализации парсера GHC (например, расположение комментариев Haddock), но её достаточно, чтобы понять, что синтаксически разрешено. Некоторые дополнительные наблюдения по этой грамматике:
- В настоящее время для типов конструкторов GADT не разрешено иметь вложенные
forallы или=>ы. (Например, что-то вродеMkT :: Int -> forall a. a -> Tбудет отклонено.) В результатеgadt_sigпомещает всю квантификацию и ограничения вначале с помощьюopt_forallиopt_context. Обратите внимание, что высшего порядкаforallы и=>ы разрешаются только в том случае, если они не появляются непосредственно справа от стрелки функции вprefix_gadt_body. (Например, что-то вродеMkS :: Int -> (forall a. a) -> Sразрешено, так как скобки отделяютforallот->.) -
Кроме того, конструкторы GADT не допускают внешних скобок, окружающих
opt_forallилиopt_ctxt, если хотя бы один из них используется. Например,MkU :: (forall a. a -> U)будет отклонено, так как это будет рассматриватьforallкак вложенный.Обратите внимание, что использование скобок в
prefix_gadt_bodyдопустимо. Например,MkV1 :: forall a. (a) -> (V1)допустимо, как иMkV2 :: forall a. (a -> V2). - Стрелки функций в
prefix_gadt_body, а также стрелка функции вrecord_gadt_body, должны использоваться инфиксным способом. Например,MkA :: (->) Int Aбудет отклонено. -
GHC использует стрелки функций в
prefix_gadt_bodyиprefix_gadt_bodyдля синтаксической разметки типов функции и результата. Обратите внимание, что GHC не пытается быть умным, просматривая синонимы типов здесь. Если вы попытаетесь это сделать, например:type C = Int -> B data B where MkB :: C
то GHC интерпретирует тип возвращаемого значения
MkBкакC, и поскольку GHC требует, чтобы тип возвращаемого значения начинался сB, это будет отклонено. С другой стороны, использование синонимов типов в самих типах аргументов и результата допускается, поэтому следующее разрешено:type B1 = Int type B2 = B data B where MkB :: B1 -> B2
- GHC примет любую комбинацию
!/~и{-# UNPACK #-}/{-# NOUNPACK #-}, хотя GHC проигнорирует некоторые комбинации. Например, GHC выдаст предупреждение, если вы напишете{-# UNPACK #-} ~Intи поступите так, как будто вы написалиInt.