-
GADTSyntax -
Разрешает использовать синтаксис GADT в определениях типов данных (но не сами GADT; см.
GADTs)
При включённом расширении GADTSyntax GHC позволяет объявлять алгебраический тип данных, явно задавая сигнатуры типов его конструкторов. Например:
data Maybe a where
Nothing :: Maybe a
Just :: a -> Maybe a
newtype Down a where
Down :: a -> Down a
Эта форма называется «объявлением в стиле GADT», поскольку обобщённые алгебраические типы данных, описанные в разделе Обобщённые алгебраические типы данных (GADT), можно объявлять только в этой форме.
Обратите внимание, что синтаксис в стиле GADT обобщает экзистенциальные типы (Конструкторы данных с экзистенциальной квантификацией). Например, следующие два объявления эквивалентны:
data Foo = forall a. MkFoo a (a -> Bool)
data Foo' where { MKFoo :: a -> (a->Bool) -> Foo' }
Любой тип данных (или newtype), который можно объявить в стандартном синтаксисе 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 гораздо полезнее и интуитивнее.
Обратите внимание, что ограничения из раздела Ограничения по-прежнему действуют; например, newtype, объявленный с помощью GADTSyntax, не может использовать экзистенциальную квантификацию.
6.4.8.1. Формальный синтаксис GADT
Чтобы точнее определить, что разрешено и что запрещено внутри конструктора в стиле GADT, ниже приведена грамматика GADT в стиле BNF. Обратите внимание, что в будущем эта грамматика может измениться.
gadt_con ::= conids '::' foralls opt_ctxt gadt_body
conids ::= conid
| conid ',' conids
foralls ::= <empty>
| forall_telescope foralls
forall_telescope ::= 'forall' tv_bndrs_spec '.'
| 'forall' tv_bndrs '->' -- with RequiredTypeArguments
tv_bndrs ::= <empty> | tv_bndr tv_bndrs
tv_bndrs_spec ::= <empty> | tv_bndr_spec tv_bndrs_spec
tv_bndr ::= tyvar
| '(' tyvar '::' ctype ')'
tv_bndr_spec ::= 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указывает всю квантификацию и ограничения в начале с помощьюforallsиopt_context. Обратите внимание, чтоforallи=>более высокого ранга разрешены, только если они не стоят непосредственно справа от стрелки функции вprefix_gadt_body. (Например, выражение видаMkS :: Int -> (forall a. a) -> Sдопустимо, поскольку скобки отделяютforallот->.) -
Кроме того, для конструкторов GADT не разрешены внешние скобки, охватывающие
forallsили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.
6.4.8.2. Дополнительные замечания о синтаксисе GADT
В оставшейся части раздела приводятся дополнительные сведения об объявлениях типов данных в стиле GADT.
- Тип результата каждого конструктора данных должен начинаться с определяемого конструктора типа. Если тип результата всех конструкторов имеет вид
T a1 ... an, гдеa1 ... an— различные переменные типа, то тип данных является обычным; в противном случае это обобщённый тип данных (Обобщённые алгебраические типы данных (GADT)). -
Как и для других сигнатур типов, можно задать одну сигнатуру для нескольких конструкторов данных. В этом примере одна сигнатура задаётся для
T1иT2:data T a where T1,T2 :: a -> T a T3 :: T a
-
Сигнатура типа каждого конструктора независима и, как обычно, универсально квантифицируется неявно. В частности, переменная(ые) типа в заголовке «
data T a where» не имеют области видимости, а у разных конструкторов могут быть разные универсально квантифицированные переменные типа:data T a where -- The 'a' has no scope T1,T2 :: b -> T b -- Means forall b. b -> T b T3 :: T a -- Means forall a. T a
-
Сигнатура конструктора может содержать ограничения классов типов, которые могут различаться у разных конструкторов. Например, следующий вариант допустим:
data T a where T1 :: Eq b => b -> b -> T b T2 :: (Show c, Ix c) => c -> [c] -> T c
При сопоставлении с образцом эти ограничения становятся доступны для удовлетворения ограничений в теле сопоставления. Например:
f :: T a -> String f (T1 x y) | x==y = "yes" | otherwise = "no" f (T2 a b) = show aОбратите внимание, что
fне перегружен; ограничениеEq, возникающее при использовании==, удовлетворяется сопоставлением с образцомT1. Аналогично, ограничениеShow, возникающее при использованииshow. -
В отличие от объявления типа данных в стиле Haskell 98, переменная(ые) типа в заголовке «
data Set a where» не имеют области видимости. Вместо этого можно указать сигнатуру рода:data Set :: Type -> Type where ...
или даже сочетать оба варианта:
data Bar a :: (Type -> Type) -> Type where ...
Для переменных типа (если они указаны) можно явно задать род, поэтому заголовок для
Fooможно записать и так:data Bar a (b :: Type -> Type) where ...
-
Можно использовать аннотации строгости в соответствующих местах типа конструктора:
data Term a where Lit :: !Int -> Term Int If :: Term Bool -> !(Term a) -> !(Term a) -> Term a Pair :: Term a -> Term b -> Term (a,b) -
В объявлении типа данных в стиле GADT можно использовать предложение
deriving. Например, следующие два объявления эквивалентны:data Maybe1 a where { Nothing1 :: Maybe1 a ; Just1 :: a -> Maybe1 a } deriving( Eq, Ord ) data Maybe2 a = Nothing2 | Just2 a deriving( Eq, Ord ) -
Сигнатура типа может содержать квантифицированные переменные типа, которые не встречаются в типе результата:
data Foo where MkFoo :: a -> (a->Bool) -> Foo Nil :: Foo
Здесь переменная типа
aне встречается в типе результата ни одного из конструкторов. Хотя в типе конструктора она универсально квантифицирована, такую переменную типа часто называют «экзистенциальной». Действительно, приведённое выше объявление задаёт в точности тот же тип, что иdata Fooв разделе Конструкторы данных с экзистенциальной квантификацией.Тип, разумеется, может также содержать контекст класса:
data Showable where MkShowable :: Show a => a -> Showable
-
В объявлении типа данных в стиле GADT можно использовать синтаксис записей:
data Person where Adult :: { name :: String, children :: [Person] } -> Person Child :: Show a => { name :: !String, funny :: a } -> PersonКак обычно, для каждого конструктора с полем
fтип поляfдолжен быть одинаковым (с точностью до альфа-переименования). КонструкторChildвыше показывает, что сигнатура, как и в случае без записей, может содержать контекст, экзистенциально квантифицированные переменные и аннотации строгости. (Примечание: «тип» после двойного двоеточия на самом деле не является типом из-за синтаксиса записей и аннотаций строгости. «Тип» такой формы может встречаться только в сигнатуре конструктора.) - Обновление записей разрешено для объявлений в стиле GADT только для тех полей, которые удовлетворяют следующему свойству: тип поля не содержит экзистенциальных переменных типа.
-
Как и в случае экзистенциальных типов, объявленных с использованием синтаксиса записей в стиле Haskell 98 (Конструкторы-записи), функции-селекторы записей генерируются только для тех полей, для которых такие селекторы имеют корректный тип. Вот пример из того раздела, записанный в синтаксисе GADT:
data Counter a where NewCounter :: { _this :: self , _inc :: self -> self , _display :: self -> IO () , tag :: a } -> Counter aКак и прежде, здесь генерируется только одна функция-селектор — для
tag. Тем не менее все имена полей можно использовать при сопоставлении с образцом и создании записей. -
В объявлении типа данных в стиле GADT нет очевидного способа указать, что конструктор данных должен быть инфиксным. Это имеет значение при выводе
Showдля типа. (Конструкторы данных, объявленные в инфиксной форме, отображаются в инфиксной форме при выводеshow.) Поэтому в GHC реализовано следующее решение: конструктор данных, объявленный в объявлении типа данных в стиле GADT, отображается в инфиксной форме при выводеShowтогда и только тогда, когда (а) он является операторным символом, (б) у него два аргумента и (в) для него задана пользователем декларация фиксированности. Например:infix 6 (:--:) data T a where (:--:) :: Int -> Bool -> T Int