Объявление экземпляра имеет вид
instance (assertion1, ..., assertionn) => class type1 ... typem where ...
Часть перед «=>» является контекстом, а часть после «=>» — заголовком объявления экземпляра.
Когда GHC пытается разрешить, скажем, ограничение C Int Bool, он пытается сопоставить каждое объявление экземпляра с ограничением, инстанцируя заголовок объявления экземпляра. Рассмотрим эти объявления:
instance context1 => C Int a where ... -- (A) instance context2 => C a Bool where ... -- (B)
По умолчанию GHC требует, чтобы ровно один экземпляр соответствовал ограничению, которое он пытается разрешить. Например, ограничение C Int Bool соответствует экземплярам (A) и (B), и поэтому будет отклонено; в то время как C Int Char соответствует только (A), и поэтому (A) выбирается.
Обратите внимание, что
- При сопоставлении GHC не учитывает контекст объявления экземпляра (
context1и т. д.). - Совпадение (путем включения как объявлений (A), так и (B)) вполне допустимо; ошибка сообщается только в том случае, если определённое ограничение соответствует более чем одному экземпляру.
См. также Перекрывающиеся экземпляры для флагов, которые ослабляют правила разрешения экземпляров.
6.8.8.1. Упрощённые правила для заголовка экземпляра
-
TypeSynonymInstances -
- Следует из:
- С:
-
6.8.1
- Статус:
Позволяет определять экземпляры типов классов для синонимов типов.
-
FlexibleInstances -
- Подразумевает:
- С:
-
6.8.1
- Статус:
Позволяет определять экземпляры типов классов с произвольными вложенными типами в заголовке экземпляра.
В Haskell 2010 заголовок объявления экземпляра должен иметь вид C (T a1 ... an), где T — конструктор типа данных, а a1 ... an — различные переменные типа. В случае многопараметрических типов классов это правило применяется к каждому параметру заголовка экземпляра (можно было бы позволить только одному параметру иметь этот вид, а другие быть переменными типов, но на данный момент это правила).
GHC ослабляет это правило двумя способами:
-
С расширением
TypeSynonymInstancesзаголовки экземпляров могут использовать синонимы типов. Как всегда, использование синонима типа просто сокращённая запись правой части определения синонима типа. Например:type Point a = (a,a) instance C (Point a) where ...
является допустимым. Объявление экземпляра эквивалентно
instance C (a,a) where ...
Как и раньше, синонимы типов должны быть полностью применены. Например, нельзя написать:
instance Monad Point where ...
-
Расширение
FlexibleInstancesпозволяет упоминать произвольные вложенные типы в заголовке объявления экземпляра. Например, это становится допустимым объявлением экземпляраinstance C (Maybe Int) where ...
См. также правила перекрытия.
Расширение
FlexibleInstancesподразумеваетTypeSynonymInstances.
Однако объявление экземпляра все еще должно соответствовать правилам завершения экземпляра: см. Правила завершения экземпляров.
6.8.8.2. Формальная синтаксис для типов объявлений экземпляров
Верхняя часть объявления экземпляра допускает только очень специфические формы типов. Чтобы точнее указать допустимые формы типов, ниже приведена грамматика в стиле BNF для вершин объявлений экземпляров.
inst_top ::= 'instance' opt_forall opt_ctxt inst_head opt_where
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
inst_head ::= '(' inst_head ')'
| prefix_cls_tycon arg_types
| arg_type infix_cls_tycon arg_type
| '(' arg_type infix_cls_tycon arg_type ')' arg_types
arg_types ::= <empty>
| arg_type arg_types
opt_where ::= <empty>
| 'where'
Где:
-
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. -
arg_type— тип, который не может иметьforallили=> -
prefix_cls_tycon— конструктор типа класса, написанный префиксом (например,Showили(&&&)), аinfix_cls_tycon— конструктор типа класса, написанный инфикс (например,\`Show\`или&&&).
Это упрощённая грамматика, которая не полностью рассматривает все детали реализации парсера GHC (например, размещение комментариев Haddock), но её достаточно для понимания того, что синтаксически разрешено. Некоторые дополнительные наблюдения по этой грамматике:
-
Объявления экземпляров не могут быть объявлены с вложенными
forallили=>. Например, это будет отклонено:instance forall a. forall b. C (Either a b) where ...
В результате
inst_topразмещает все квантификации и ограничения впередиopt_forallиopt_context. -
Кроме того, типы объявлений экземпляров не допускают внешних скобок, окружающих
opt_forallилиopt_ctxt, если хотя бы один из них используется. Например,instance (forall a. C a)будет отклонено, так как GHC будет рассматриватьforallкак вложенный.Обратите внимание, что использование скобок в
inst_headдопустимо. Например,instance (C a)принимается, как иinstance forall a. (C a).
6.8.8.3. Правила завершения экземпляров
Независимо от FlexibleInstances и FlexibleContexts, объявления экземпляров должны соответствовать некоторым правилам, которые гарантируют, что разрешение экземпляров завершится. Ограничения могут быть сняты с помощью UndecidableInstances (см. Неразрешимые экземпляры и циклические надклассы).
Эти правила таковы:
-
Условия Патерсона: для каждого ограничения класса
(C t1 ... tn)в контексте- Ни одна переменная типа не имеет больше вхождений в ограничение, чем в заголовке
- Ограничение имеет меньше конструкторов и переменных (взятых вместе и учитывая повторения), чем заголовок
- Ограничение не упоминает функции типов. Применение функции типа, в принципе, может расшириться до типа произвольного размера, и поэтому они отклоняются сразу
Если эти три условия выполняются, мы говорим, что ограничение
(C t1 ... tn)меньше Патерсона, чем заголовок экземпляра. - Условие покрытия. Для каждой функциональной зависимости ⟨tvs⟩left
->⟨tvs⟩right класса, каждая переменная типа в S(⟨tvs⟩right) должна появляться в S(⟨tvs⟩left), где S — подстановка, отображающая каждую переменную типа в объявлении класса на соответствующий тип в заголовке экземпляра.
Эти ограничения гарантируют завершение разрешения экземпляров: каждый шаг сокращения делает задачу меньше по крайней мере на один конструктор. Вы можете найти много справочных материалов о причинах этих ограничений в статье Understanding functional dependencies via Constraint Handling Rules.
Например, это допустимо:
instance C Int [a] -- Multiple parameters
instance Eq (S [a]) -- Structured type in head
-- Repeated type variable in head
instance C4 a a => C4 [a] [a]
instance Stateful (ST s) (MutVar s)
-- Head can consist of type variables only
instance C a
instance (Eq a, Show b) => C2 a b
-- Non-type variables in context
instance Show (s a) => Show (Sized s a)
instance C2 Int a => C3 Bool [a]
instance C2 Int a => C3 [a] b
Но это нет:
-- Context assertion no smaller than head
instance C a => C a where ...
-- (C b b) has more occurrences of b than the head
instance C b b => Foo [b] where ...
Те же ограничения применяются к экземплярам, сгенерированным с помощью deriving-клаузул. Таким образом, следующее принимается:
data MinHeap h a = H a (h a) deriving (Show)
потому что производный экземпляр
instance (Show a, Show (h a)) => Show (MinHeap h a)
соответствует вышеперечисленным правилам.
Ограничения на функциональные зависимости (Функциональные зависимости) особенно проблематичны. Искушение заключается в введении переменных типа в контексте, которые не появляются в заголовке, что исключено обычными правилами. Например:
class HasConverter a b | a -> b where convert :: a -> b data Foo a = MkFoo a instance (HasConverter a b,Show b) => Show (Foo a) where show (MkFoo value) = show (convert value)
Однако это опасная область. Например, вот программа, которая заставит типовой проверщик зациклиться:
class D a class F a b | a->b instance F [a] [[a]] instance (D c, F a c) => D [a] -- 'c' is not mentioned in the head
Аналогично, может возникнуть желание снять ограничение покрытия:
class Mul a b c | a b -> c where (.*.) :: a -> b -> c instance Mul Int Int Int where (.*.) = (*) instance Mul Int Float Float where x .*. y = fromIntegral x * y instance Mul a b c => Mul a [b] [c] where x .*. v = map (x.*.) v
Третье объявление экземпляра не удовлетворяет условию покрытия; и, действительно, следующее (довольно странное) определение:
f = \ b x y -> if b then x .*. [y] else y
заставляет вывод экземпляров зациклиться, потому что он требует ограничения (Mul a [b] b).