Spec-Zone.ru › Haskell 9

6.4.9. Типовые семейства

TypeFamilies
Подразумевает:

MonoLocalBinds, KindSignatures, ExplicitNamespaces

Подразумевается:

TypeFamilyDependencies

С:

6.8.1

Разрешают использование и определение индексированных типов и семейств данных.

Индексированные типовые семейства представляют собой расширение, облегчающее программирование на уровне типов. Типовые семейства являются обобщением ассоциированных типов данных [AssocDataTypes2005] и ассоциированных синонимов типов [AssocTypeSyn2005]. Сами типовые семейства описаны в Schrijvers 2008 [TypeFamilies2008]. Типовые семейства по сути предоставляют тип-индексированные типы данных и именованные функции над типами, что полезно для обобщенного программирования и интерфейсов библиотек с высокой степенью параметризации, а также для интерфейсов с улучшенной статической информацией, подобно зависимым типам. Их также можно рассматривать как альтернативу функциональным зависимостям, но они предоставляют более функциональный стиль программирования на уровне типов, чем реляционный стиль функциональных зависимостей.

Индексированные типовые семейства, или коротко типовые семейства, являются конструкторами типов, представляющими множества типов. Элементы множества обозначаются путем снабжения конструктора типового семейства параметрами типа, которые называются индексами типа. Разница между обычными параметризованными конструкторами типов и конструкторами семейств аналогична разнице между параметрически полиморфными функциями и (ад-хок полиморфными) методами типов классов. Параметрически полиморфные функции ведут себя одинаково во всех типах экземпляров, в то время как методы класса могут изменять свое поведение в зависимости от параметров типа класса. Аналогично, обычные конструкторы типов предполагают одинаковое представление данных для всех экземпляров типа, но конструкторы семейств могут иметь различные типы представлений для различных индексов типа.

Индексированные типовые семейства бывают трех типов: семейства данных, открытые типовые семейства-синонимы и закрытые типовые семейства-синонимы. Они являются индексированными вариантами алгебраических типов данных и синонимов типов соответственно. Экземпляры семейств данных могут быть типами данных и newtype.

Типовые семейства включаются с помощью языкового расширения TypeFamilies. Дополнительную информацию об использовании типовых семейств в GHC можно найти на странице Википедии Haskell по типовым семействам.

[AssocDataTypes2005]

“Associated Types with Class”, M. Chakravarty, G. Keller, S. Peyton Jones, and S. Marlow. В материалах «The 32nd Annual ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages (POPL’05)», страницы 1-13, ACM Press, 2005.

[AssocTypeSyn2005]

“Associated Type Synonyms”, M. Chakravarty, G. Keller, and S. Peyton Jones. В материалах «The Tenth ACM SIGPLAN International Conference on Functional Programming», ACM Press, страницы 241-253, 2005.

[TypeFamilies2008]

“Type Checking with Open Type Functions”, T. Schrijvers, S. Peyton-Jones, M. Chakravarty, and M. Sulzmann, в материалах «ICFP 2008: The 13th ACM SIGPLAN International Conference on Functional Programming», ACM Press, страницы 51-62, 2008.

6.4.9.1. Семейства данных

Семейства данных встречаются в двух вариантах: (1) они могут быть определены на верхнем уровне или (2) они могут появляться внутри типов классов (в этом случае они известны как связанные типы). Первый вариант является более общим, так как не требует совпадения индексов типов с параметрами класса. Однако второй вариант может привести к более структурированному коду и предупреждениям компилятора, если некоторые экземпляры типов были — возможно, случайно — пропущены. В дальнейшем мы всегда сначала обсудим общий вид на верхнем уровне, а затем рассмотрим дополнительные ограничения, наложенные на связанные типы.

6.4.9.1.1. Объявления семейств данных

Индексированные семейства данных вводятся с помощью подписи, например:

data family GMap k :: Type -> Type

Специальный символ family отличает семейство от стандартных объявлений данных. Аннотация вида результата необязательна и, как обычно, по умолчанию равна Type в случае пропуска. Пример:

data family Array e

Имена аргументов также могут иметь явные подписи видов, если это необходимо. Точно так же, как и с объявлениями GADT, именованные аргументы полностью необязательны, так что мы можем объявить Array альтернативно с

data family Array :: Type -> Type

В отличие от обычных определений данных, вид результата семейства данных не обязательно должен быть Type. Он может быть:

  • В форме TYPE r для некоторого r (см. Полиморфизм представления). Например:

    data family DF1 :: TYPE IntRep
    data family DF2 (r :: RuntimeRep)  :: TYPE r
    data family DF3 :: Type -> TYPE WordRep
    
  • Голым переменной вида (с включенным PolyKinds). Например:

    data family DF4 :: k
    data family DF5 (a :: k) :: k
    data family DF6 :: (k -> Type) -> k
    

Однако виды экземпляров данных должны заканчиваться на Type. Это ограничение немного ослаблено, когда включен расширение UnliftedNewtypes, так как оно позволяет виду newtype instance заканчиваться на TYPE r для некоторого r.

6.4.9.1.2. Объявления экземпляров данных

Объявления экземпляров семейств данных и новых типов очень похожи на стандартные объявления данных и новых типов. Единственное различие заключается в том, что ключевое слово data или newtype следует за instance, и что некоторые или все аргументы типа могут быть типами без переменных, но не могут содержать универсальные типы или семейства синонимов типов. Однако семейства данных обычно допускаются в параметрах типов, а синонимы типов допускаются, если они полностью применены и раскладываются до типа, который сам по себе допустим — точно так же, как это требуется для вхождений синонимов типов в параметры экземпляров классов. Например, экземпляр Either для GMap выглядит так:

data instance GMap (Either a b) v = GMapEither (GMap a v) (GMap b v)

В этом примере объявление имеет только один вариант. В общем случае их может быть любое количество.

Когда включено ExplicitForAll, переменные типа и вида, используемые в левой части, могут быть явно связаны. Например:

data instance forall a (b :: Proxy a). F (Proxy b) = FProxy Bool

Если присутствует явная forall, все упоминаемые переменные типа и вида, которые не находятся уже в области видимости, должны быть связаны forall:

data instance forall   (a :: k). F a = FOtherwise  -- rejected: k not in scope
data instance forall k (a :: k). F a = FOtherwise  -- accepted

Если включен флаг -Wunused-type-patterns, переменные типа, которые упоминаются в шаблонах слева, но не используются справа, будут сообщаться. Переменные, которые встречаются несколько раз слева, также считаются используемыми. Для подавления предупреждений неиспользуемые переменные следует либо заменить, либо добавить перед ними знак подчеркивания. Переменные типа, начинающиеся с подчеркивания (_x), в противном случае обрабатываются как обычные переменные типа.

Это напоминает подстановки, которые могут использоваться в Частичных сигнатурах типов. Однако есть некоторые различия. Не генерируется никаких сообщений об ошибках, сообщающих о выведенных типах, и расширение PartialTypeSignatures не оказывает никакого влияния.

Переменная типа или вида, явно связанная с помощью ExplicitForAll, но не используемая в левой части, вызовет ошибку, а не предупреждение.

Объявления экземпляров данных и новых типов допускаются только при наличии соответствующего объявления семейства в области видимости — точно так же, как объявление экземпляра класса требует видимости объявления класса. Кроме того, каждое объявление экземпляра должно соответствовать виду, определяемому его объявлением семейства. Это означает, что количество параметров объявления экземпляра должно соответствовать арности, определенной видом семейства.

Объявление экземпляра семейства данных может использовать всю выразительность обычных data или newtype объявлений:

  • Хотя семейство данных вводится с помощью ключевого слова «data», экземпляр семейства данных может использовать data или newtype. Например:

    data family T a
    data    instance T Int  = T1 Int | T2 Bool
    newtype instance T Char = TC Bool
    
  • Экземпляр data instance может использовать синтаксис GADT для конструкторов данных и, в действительности, может определить GADT. Например:

    data family G a b
    data instance G [a] b where
       G1 :: c -> G [Int] b
       G2 :: G [a] Bool
    
  • Можно использовать deriving предложение в объявлении data instance или newtype instance.

Даже если семейства данных определены как объявления на верхнем уровне, функции, выполняющие различные вычисления для разных экземпляров семейства, могут по-прежнему нуждаться в определении как методов типов классов. В частности, следующее невозможно:

data family T a
data instance T Int  = A
data instance T Char = B
foo :: T a -> Int
foo A = 1
foo B = 2

Вместо этого вам нужно написать foo как операцию класса, таким образом:

class Foo a where
  foo :: T a -> Int
instance Foo Int where
  foo A = 1
instance Foo Char where
  foo B = 2

Учитывая функциональность, предоставляемую GADTs (обобщенными алгебраическими типами данных), может показаться, что такое определение должно быть выполнимо. Однако семейства типов, в отличие от GADTs, являются открытыми; т.е. новые экземпляры всегда можно добавлять, возможно, в других модулях. Поддержка сопоставления с образцом для разных экземпляров данных потребовала бы форму расширяемого оператора case.

6.4.9.1.3. Перекрытие экземпляров данных

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

6.4.9.2. Семейства синонимов

Семейства типов представлены в трех вариантах: (1) они могут быть определены как открытые семейства на верхнем уровне, (2) они могут быть определены как закрытые семейства на верхнем уровне или (3) они могут появляться внутри типов классов (в этом случае они известны как связанные синонимы типов). Семейства на верхнем уровне более общие, так как не требуют совпадения индексов типов с параметрами класса. Однако связанные синонимы типов могут привести к более структурированному коду и предупреждениям компилятора, если некоторые экземпляры типов были - возможно, случайно - опушены. В дальнейшем мы всегда сначала обсудим общие формы на верхнем уровне, а затем рассмотрим дополнительные ограничения, наложенные на связанные типы. Обратите внимание, что закрытые связанные синонимы типов не существуют.

6.4.9.2.1. Декларации семейств типов

Открытые индексированные семейства типов вводятся с помощью сигнатуры, например

type family Elem c :: Type

Специальный family отличает семейства от стандартных деклараций типов. Аннотация вида результата необязательна и, как обычно, по умолчанию равна Type при ее отсутствии. Пример:

type family Elem c

Параметры также могут быть заданы с явными сигнатурами вида, если это необходимо. Мы называем количество параметров в декларации семейства типов арифметикой семейства, и все применения семейства типов должны быть полностью насыщены относительно этой арифметики. Это требование отличается от обычных синонимов типов, и это подразумевает, что вид семейства типов недостаточен для определения арифметики семейства и, следовательно, вообще недостаточен для определения того, является ли применение семейства типов корректным. В качестве примера рассмотрим следующую декларацию:

type family F a b :: Type -> Type
  -- F's arity is 2,
  -- although its overall kind is Type -> Type -> Type -> Type

Учитывая эту декларацию, вот примеры корректных и некорректных типов:

F Char [Int]       -- OK!  Kind: Type -> Type
F Char [Int] Bool  -- OK!  Kind: Type
F IO Bool          -- WRONG: kind mismatch in the first argument
F Bool             -- WRONG: unsaturated application

Аннотация вида результата необязательна и по умолчанию равна Type (как и виды аргументов) при ее отсутствии. Поливидные семейства типов могут быть объявлены с помощью параметра в аннотации вида:

type family F a :: k

В этом случае параметр вида k фактически является неявным параметром семейства типов.

В месте определения арифметика определяет, какие входные данные могут быть сопоставлены:

data PT (a :: Type)

type family F1 :: k -> Type
type instance F1 = PT
  -- OK, 'k' can be matched on.

type family F0 :: forall k. k -> Type
type instance F0 = PT
  -- Error:
  --   • Expected kind ‘forall k. k -> Type’,
  --       but ‘PT’ has kind ‘Type -> Type’
  --   • In the type ‘PT’
  --     In the type instance declaration for ‘F0’

Оба F1 и F0 имеют вид forall k. k -> Type, но их арифметика отличается.

В местах использования арифметика определяет, может ли определение использоваться в сценарии высшего порядка:

type HRK (f :: forall k. k -> Type) = (f Int, f Maybe, f True)

type H1 = HRK F0  -- OK
type H2 = HRK F1
  -- Error:
  --   • Expected kind ‘forall k. k -> Type’,
  --       but ‘F1’ has kind ‘k0 -> Type’
  --   • In the first argument of ‘HRK’, namely ‘F1’
  --     In the type ‘HRK F1’
  --     In the type declaration for ‘H2’

Это следствие требования, чтобы все применения семейства типов были полностью насыщены относительно их арифметики.

6.4.9.2.2. Декларации экземпляров типов

Декларации экземпляров семейств типов очень похожи на стандартные декларации синонимов типов. Единственные два отличия заключаются в том, что ключевое слово type следует за instance и что некоторые или все аргументы типов могут быть типами без переменных, но не могут содержать типов forall или семейств синонимов типов. Однако семейства данных, как правило, разрешены, а синонимы типов разрешены, только если они полностью применены и раскрыты до типа, который допустим — это те же самые требования, что и для экземпляров данных. Например, экземпляр [e] для Elem это

type instance Elem [e] = e

Аргументы типов можно заменить подчеркиванием (_), если имена аргументов не имеют значения. Это то же самое, что писать переменные типа с уникальными именами. Неиспользуемые аргументы типов можно заменить или префиксровать подчеркиванием, чтобы избежать предупреждений, когда включен флаг -Wunused-type-patterns. Применяются те же правила, что и для Деклараций экземпляров данных.

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

Декларации экземпляров семейств типов допустимы только при наличии соответствующей декларации семейства в области видимости — так же, как экземпляры классов требуют видимости декларации класса. Более того, каждая декларация экземпляра должна соответствовать виду, определяемому его декларацией семейства, и количество параметров типа в декларации экземпляра должно совпадать с количеством параметров типа в декларации семейства. Наконец, правая часть экземпляра типа должна быть монотипом (т. е. она не может включать forall), и после расширения всех насыщенных синонимов типов vanilla, никаких синонимов, кроме синонимов семейств, не должно остаться.

6.4.9.2.3. Закрытые семейства типов

Семейство типов также можно объявить с помощью where-записи, определяющей полный набор уравнений для этого семейства. Например:

type family F a where
  F Int  = Double
  F Bool = Char
  F a    = String

Уравнения закрытого семейства типов перебираются по порядку сверху вниз при упрощении применения семейства типов. В этом примере мы объявляем экземпляр для F, так что F Int упрощается до Double, F Bool упрощается до Char, и для любого другого типа a, который не является Int или Bool, F a упрощается до String. Обратите внимание, что GHC должен быть уверен, что a не может быть унифицировано с Int или Bool в этом последнем случае; если программист укажет только F a в своем коде, GHC не сможет упростить тип. В конце концов, a позже может быть проинициализирован значением Int.

Уравнения закрытого семейства типов имеют те же ограничения и расширения, что и уравнения для экземпляров открытых семейств типов. Например, при включении ExplicitForAll переменные типа или вида, используемые в левой части уравнения, могут быть явно связаны, например, в:

type family R a where
  forall t a. R (t a) = [a]
  forall a.   R a     = a

Закрытое семейство типов может быть объявлено без уравнений. Такие закрытые семейства типов представляют собой неявные определения на уровне типов, которые никогда не будут сокращены, не обязательно являются инъективными (в отличие от пустых типов данных) и не могут быть присвоены экземпляры.

В файле hs-boot, закрытые семейства типов должны либо иметь те же уравнения, что и в исходном файле, либо вы можете использовать следующий синтаксис для пропуска уравнений (обратите внимание на буквальное ..)

type family R a where ..

В этом случае закрытое семейство типов R с этой декларацией «запуска» может иметь любое количество уравнений, заданных в исходном файле hs (включая ноль). Дополнительную информацию о взаимно рекурсивных модулях с hs-boot модулями (включая семейства типов) см. в Взаимно рекурсивные модули и файлы hs-boot.

6.4.9.2.4. Примеры семейств типов

Вот несколько примеров допустимых и недопустимых экземпляров типов:

type family F a :: Type
type instance F [Int]   = Int   -- OK!
type instance F String  = Char  -- OK!
type instance F (F a)   = a     -- WRONG: type parameter mentions a type family
type instance
  F (forall a. (a, b))  = b     -- WRONG: a forall type appears in a type parameter
type instance
  F Float = forall a.a          -- WRONG: right-hand side may not be a forall type
type family H a where          -- OK!
  H Int  = Int
  H Bool = Bool
  H a    = String
type instance H Char = Char    -- WRONG: cannot have instances of closed family
type family K a where          -- OK!

type family G a b :: Type -> Type
type instance G Int            = (,)     -- WRONG: must be two type parameters
type instance G Int Char Float = Double  -- WRONG: must be two type parameters

6.4.9.2.5. Совместимость и различие уравнений семейств типов

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

  1. все соответствующие типы и неявные виды в шаблонах различны, или
  2. два шаблона унифицируются, создавая подстановку, и правые части равны при этой подстановке.

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

Первый пункт «совместимости» более прямой. Он утверждает, что шаблоны двух разных экземпляров семейств типов не могут перекрываться. Например, следующее запрещено:

type instance F Int = Bool
type instance F Int = Char

Второй пункт немного интереснее. Он утверждает, что два перекрывающихся экземпляра семейств типов разрешены, если правые части совпадают в области перекрытия. Некоторые примеры помогут здесь:

type instance F (a, Int) = [a]
type instance F (Int, b) = [b]   -- overlap permitted

type instance G (a, Int)  = [a]
type instance G (Char, a) = [a]  -- ILLEGAL overlap, as [Char] /= [Int]

Обратите внимание, что это условие совместимости не зависит от того, является ли семейство типов связанным или нет, и это не только вопрос согласованности, но и вопрос безопасности типов.

Для поливидного семейства типов виды проверяются на различие так же, как и типы. Например, следующее принимается:

type family J a :: k
type instance J Int = Bool
type instance J Int = Maybe

Эти экземпляры совместимы, потому что они отличаются своим неявным параметром вида; первый использует Type, а второй использует Type -> Type.

Определение «совместимости» использует понятие «различие», определение которого в свою очередь опирается на редукцию семейства типов. Это условие «различия», как заявлено, невозможно проверить, поэтому мы используем эту консервативную аппроксимацию: два типа считаются различными, когда два типа не могут быть унифицированы, даже бесконечным унификатором. Разрешение бесконечного унификатора запрещает следующую пару экземпляров:

type instance H x   x = Int
type instance H [x] x = Bool

Шаблоны типов в этой паре равны, если x заменяется бесконечным вложением списков. Отклонение таких экземпляров необходимо для безопасности типа.

Совместимость также влияет на закрытые семейства типов. При упрощении применения закрытого семейства типов GHC будет выбирать уравнение только тогда, когда он уверен, что ни одно несовместимое предыдущее уравнение никогда не будет применено. Вот несколько примеров:

type family F a where
  F Int = Bool
  F a   = Char

type family G a where
  G Int = Int
  G a   = a

В определении для F, два уравнения несовместимы — их шаблоны не различны, и тем не менее их правые части не совпадают. Таким образом, прежде чем GHC выберет второе уравнение, он должен быть уверен, что первое никогда не будет применено. Поэтому тип F a не упрощается; только тип, такой как F Double упростится до Char. В G, с другой стороны, два уравнения совместимы. Таким образом, GHC может игнорировать первое уравнение при рассмотрении второго. Поэтому G a упростится до a.

Несовместимости между уравнениями закрытых семейств типов могут быть отображены в :info, когда включен -fprint-axiom-incomps.

Однако см. Декларации типов, классов и других для правил перекрытия в GHCi.

6.4.9.2.6. Определяемость экземпляров типов-синонимов

Для обеспечения того, чтобы выведение типов при наличии семейств типов было разрешимой задачей, необходимо ввести ряд дополнительных ограничений на формирование объявлений экземпляров типов (см. определение 5 (Упрощенные условия) в работе "Проверка типов с открытыми функциями типов”). Объявления экземпляров имеют общий вид

type instance F t1 .. tn = t

где требуется, чтобы для каждого применения семейства типов (G s1 .. sm) в t,

  1. s1 .. sm не содержали никаких конструкторов семейств типов,
  2. общее число символов (конструкторы типов данных и переменные типов) в s1 .. sm строго меньше, чем в t1 .. tn, и
  3. для каждой переменной типа a, a встречается в s1 .. sm не чаще, чем в t1 .. tn.

Эти ограничения легко проверяются и гарантируют завершение вывода типов. Однако их недостаточно для обеспечения полноты вывода типов в присутствии так называемых «замкнутых равенств», таких как a ~ [F a], где рекуррентное вхождение переменной типа находится под применением семейства типов и применением конструктора данных — см. упомянутую выше статью для получения подробностей.

Если опция UndecidableInstances передается компилятору (см. Неразрешимые экземпляры и замкнутые суперклассы), вышеупомянутые ограничения не применяются, и программисту необходимо убедиться в завершении нормализации семейств типов во время вывода типов.

6.4.9.2.7. Сокращение применений семейств типов

-ffamily-application-cache

Флаг -ffamily-application-cache (включен по умолчанию) сообщает GHC использовать кэш при сокращении применений семейств типов. В большинстве случаев это ускорит компиляцию. Использование этого флага не повлияет на поведение во время выполнения.

Когда GHC сталкивается с применением семейства типов (например, F Int a) в программе, ему часто необходимо его сократить для завершения проверки типов. Вот простой пример:

type family F a where
  F Int            = Bool
  F (Maybe Double) = Char

g :: F Int -> Bool
g = not

Несмотря на то, что тип g упоминает F Int, GHC должен распознать, что аргумент g на самом деле имеет тип Bool. Это делается путем сокращения F Int до Bool. Иногда информации недостаточно для сокращения применения семейства типов; мы говорим, что такое применение заблокировано. В продолжение этого примера, вхождение F (Maybe a) (для некоторой переменной типа a) было бы заблокировано, так как ни одно уравнение не применяется.

Во время проверки типов GHC использует эвристики для определения следующего применения семейства типов для сокращения; нет предсказуемого порядка между различными применениями семейств типов. Недетерминированность редко имеет значение на практике. В большинстве программ сокращение семейств типов завершается, поэтому эти решения несущественны. Однако, если применение семейства типов не завершается, возможна непредсказуемая расходимость проверки типов. (GHC всегда выбирает тот же путь для данной программы, но небольшие изменения в этой программе могут заставить GHC выбрать другой путь. Компиляция данной, неизменной программы по-прежнему является детерминированной.)

Для ускорения сокращения семейств типов GHC обычно использует кэш, запоминая, какие применения семейств типов он ранее сократил. Эту функцию можно отключить с помощью -fno-family-application-cache.

6.4.9.3. Подстановочные знаки в левой части объявлений экземпляров данных и семейств типов

Когда имя аргумента типа в объявлении экземпляра данных или семейства типов не имеет значения, его можно заменить подстановочным знаком (_). Это то же самое, что и запись переменной типа с уникальным именем.

data family F a b :: Type
data instance F Int _ = Int
-- Equivalent to  data instance F Int b = Int

type family T a :: Type
type instance T (a,_) = a
-- Equivalent to  type instance T (a,b) = a

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

Переменная типа, начинающаяся с подстановочного знака, не обрабатывается специально в объявлении экземпляра типа или данных. Например:

data instance F Bool _a = _a -> Int
-- Equivalent to  data instance F Bool a = a -> Int

Проследите разницу со специальной обработкой именованных подстановочных знаков в сигнатурах типов (Именованные подстановочные знаки).

6.4.9.4. Связанные данные и семейства типов

Семейство данных или синонимов типов может быть объявлено как часть класса типов, например:

class GMapKey k where
  data GMap k :: Type -> Type
  ...

class Collects ce where
  type Elem ce :: Type
  ...

При этом мы (по желанию) можем опустить ключевое слово «family».

Все параметры типа, конечно, должны быть переменными типа, и некоторые (но не обязательно все) из них могут быть параметрами класса. Каждый параметр класса может быть использован не более одного раза на связанный тип, но некоторые могут быть опущены, и они могут быть в другом порядке, чем в заголовке класса. Следовательно, следующий искусственный пример допустим:

class C a b c where
  type T c a x :: Type

Здесь c и a являются параметрами класса, но тип также индексируется по третьему параметру x.

6.4.9.4.1. Связанные экземпляры

Когда связанное семейство данных или синонимов типов объявляется внутри экземпляра класса типов, мы (по желанию) можем опустить ключевое слово instance в экземпляре семейства:

instance (GMapKey a, GMapKey b) => GMapKey (Either a b) where
  data GMap (Either a b) v = GMapEither (GMap a v) (GMap b v)
  ...

instance Eq (Elem [e]) => Collects [e] where
  type Elem [e] = e
  ...

Экземпляр данных или семейства типов для связанного типа должен следовать правилу, согласно которому индексы типа, соответствующие параметрам класса, должны быть точно такими же, как типы, указанные в заголовке экземпляра. Например:

class Collects ce where
  type Elem ce :: Type

instance Eq (Elem [e]) => Collects [e] where
  -- Choose one of the following alternatives:
  type Elem [e] = e       -- OK
  type Elem [x] = x       -- BAD; '[x]' is different to '[e]' from head
  type Elem x   = x       -- BAD; 'x' is different to '[e]'
  type Elem [Maybe x] = x -- BAD: '[Maybe x]' is different to '[e]'

Обратите внимание на следующие моменты:

  • Экземпляр для связанного семейства может появляться только в качестве части объявлений экземпляров класса, в котором было объявлено семейство, точно так же, как и уравнения методов класса.
  • Переменные типа в правой части уравнения семейства типов должны, как обычно, быть явно связаны левой частью. Это ограничение ослаблено для переменных вида, однако, так как правая часть может упоминать переменные вида, которые неявно связаны. Например, эти варианты допустимы:

    data family Nat :: k -> k -> Type
    -- k is implicitly bound by an invisible kind pattern
    newtype instance Nat :: (k -> Type) -> (k -> Type) -> Type where
      Nat :: (forall xx. f xx -> g xx) -> Nat f g
    
    class Funct f where
      type Codomain f :: Type
    instance Funct ('KProxy :: KProxy o) where
      -- o is implicitly bound by the kind signature
      -- of the LHS type pattern ('KProxy)
      type Codomain 'KProxy = NatTr (Proxy :: o -> Type)
    
  • Экземпляр для связанного типа может быть опущен в экземплярах класса. В этом случае, если нет экземпляра по умолчанию (см. Значения по умолчанию для связанных синонимов типов), соответствующий тип экземпляра не обитаем; т.е., только расходящиеся выражения, такие как undefined, могут принять тип.
  • Хотя это необычно, может быть несколько экземпляров для связанного семейства в одном объявлении экземпляра. Например, это допустимо:

    instance GMapKey Flob where
      data GMap Flob [v] = G1 v
      data GMap Flob Int = G2 Int
      ...
    

    Здесь мы даём два объявления экземпляра данных, в одном из которых последний параметр — [v], а в другом — Int. Поскольку вы не можете дать никаких последующих экземпляров для (GMap Flob ...), эта возможность наиболее полезна, когда свободная индексированная переменная является вида с конечным числом альтернатив (в отличие от Type).

  • Когда ExplicitForAll включено, переменные типа и вида могут быть явно связаны в экземплярах связанных данных или семейств типов таким же образом (и с теми же ограничениями), как и в Объявления экземпляров данных или Объявления экземпляров типов. Например, адаптируя вышесказанное, следующее принимается:

    instance Eq (Elem [e]) => Collects [e] where
      type forall e. Elem [e] = e
    

6.4.9.4.2. Значения по умолчанию для связанных синонимов типов

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

class IsBoolMap v where
  type Key v
  type instance Key v = Int

  lookupKey :: Key v -> v -> Maybe Bool

instance IsBoolMap [(Int, Bool)] where
  lookupKey = lookup

В объявлении instance для класса, если для связанного типа не указано явное объявление type instance, вместо этого используется объявление значения по умолчанию, как и в случае с методами класса по умолчанию.

Обратите внимание на следующие моменты:

  • Ключевое слово instance необязательно.
  • Может быть не более одного объявления значения по умолчанию для связанного синонима типа.
  • Значение по умолчанию недопустимо для связанного данного типа.
  • В объявлении значения по умолчанию на левой стороне должны быть только переменные типа, а переменные типа не могут повторяться на левой стороне. На правой стороне должны упоминаться только переменные типа, которые явно связаны на левой стороне. Однако, это ограничение ослаблено для переменных вида, так как правая часть может упоминать переменные вида, которые неявно связаны на левой стороне.

    Как и со Связанными экземплярами, можно явно связывать переменные типа и вида в объявлениях значений по умолчанию с помощью forall с помощью расширения языка ExplicitForAll.

  • В отличие от самого объявления семейства связанных типов, переменные типа значения по умолчания независимы от переменных родительского класса.

Вот несколько примеров:

class C (a :: Type) where
  type F1 a :: Type
  type instance F1 a = [a]     -- OK
  type instance F1 a = a->a    -- BAD; only one default instance is allowed

  type F2 b a                  -- OK; note the family has more type
                               --     variables than the class
  type instance F2 c d = c->d  -- OK; you don't have to use 'a' in the type instance

  type F3 a
  type F3 [b] = b              -- BAD; only type variables allowed on the
                               --      LHS, and the argument to F3 is
                               --      instantiated to [b], which is not
                               --      a bare type variable

  type F4 x y
  type F4 x x = x              -- BAD; the type variable x is repeated on
                               --      the LHS

  type F5 a
  type F5 b = a                -- BAD; 'a' is not in scope  in the RHS

  type F6 a :: [k]
  type F6 a = ('[] :: [x])     -- OK; the kind variable x is implicitly
                               --     bound by an invisible kind pattern
                               --     on the LHS

  type F7 a
  type F7 a =
    Proxy ('[] :: [x])         -- BAD; the kind variable x is not bound,
                               --      even by an invisible kind pattern

  type F8 (x :: a) :: [a]
  type F8 x = ('[] :: [a])     -- OK; the kind variable a is implicitly
                               --     bound by the kind signature of the
                               --     LHS type pattern

  type F9 (a :: k)
  type F9 a = Maybe a          -- BAD; the kind variable k is
                               --      instantiated to Type, which is not
                               --      a bare kind variable

  type F10 (a :: j) (b :: k)
  type F10 (a :: z) (b :: z)
    = Proxy a                  -- BAD; the kind variable z is repeated,
                               --      as both j and k are instantiated to z

  type F11 a b
  type forall a b. F11 a b = a -- OK; LHS type variables can be
                               --     explicitly bound with 'forall'

  type F12 (a :: k)
  type F12 @k a = Proxy a      -- OK; visible kind application syntax is
                               --     permitted in default declarations

6.4.9.4.3. Область видимости параметров класса

Видимость параметров класса в правой части экземпляров связанных семейств зависит исключительно от параметров семейства. В качестве примера рассмотрим простое объявление класса

class C a b where
  data T a

Только один из двух параметров класса является параметром семейства данных. Следовательно, следующее объявление экземпляра недопустимо:

instance C [c] d where
  data T [c] = MkT (c, d)    -- WRONG!!  'd' is not in scope

Здесь правая часть экземпляра данных упоминает переменную типа d, которая не встречается в её левой части. Мы не можем принять такие экземпляры данных, так как это нарушит безопасность типов.

Заметьте, что правая часть экземпляра связанного семейства также может содержать параметры вида (используя расширение PolyKinds). Например, этот класс и экземпляр вполне допустимы:

class C k where
  type T :: k

instance C (Maybe a) where
  type T = (Nothing :: Maybe a)

Здесь, хотя правая часть (Nothing :: Maybe a) упоминает переменную вида a , которая не встречается в левой части, это приемлемо, потому что a неявно связана шаблоном вида T.

Переменная вида также может быть связана неявно в шаблоне типа LHS, как в этом примере:

class C a where
  type T (x :: a) :: [a]

instance C (Maybe a) where
  type T x = ('[] :: [Maybe a])

В ('[] :: [Maybe a]), переменная вида a неявно связана сигнатурой вида шаблона типа LHS x.

6.4.9.4.4. Контексты экземпляров и связанные экземпляры типа и данных

Объявления связанных экземпляров типа и данных не наследуют какой-либо контекст, указанный в окружающем экземпляре. Для объявлений экземпляров типа неясно, что означал бы контекст. Для объявлений экземпляров данных маловероятно, что пользователь захочет повторить контекст для каждого конструктора данных. Единственное место, где контекст, вероятно, может быть полезен, — в deriving фрагменте связанного экземпляра данных. Однако даже здесь роль внешнего контекста экземпляра неясна. Поэтому, для ясности, мы придерживаемся вышеприведённого правила: контекст окружающего экземпляра игнорируется. Если вам нужно использовать нетривиальный контекст в производном экземпляре, используйте клаузу standalone deriving (на верхнем уровне).

6.4.9.5. Импорт и экспорт

Правила для списков экспорта (документ Haskell Report Раздел 5.2) необходимо скорректировать для типов семейств:

  • Форма T(..), где T — семейство данных, называет семейство T и все конструкторы, входящие в область видимости (как квалифицированные, так и нет), которые являются экземплярами данных семейства T.
  • Форма T(.., ci, .., fj, ..), где T — семейство данных, называет T и указанные конструкторы ci и поля fj как обычно. Конструкторы и имена полей должны принадлежать какому-либо экземпляру данных семейства T, но не обязательно одному и тому же.
  • Форма C(..), где C — класс, называет класс C и все его методы и связанные типы.
  • Форма C(.., mi, .., type Tj, ..), где C — класс, называет класс C, а также указанные методы mi и связанные типы Tj. Типам нужен ключевой слово «type», чтобы отличить их от конструкторов данных.
  • В случае отсутствия списка экспорта и определённого экземпляра данных, соответствующий конструктор типа семейства данных экспортируется вместе с новыми конструкторами данных, независимо от того, определено ли семейство данных локально или в другом модуле.

6.4.9.5.1. Примеры

Вспомним наш пример класса GMapKey:

class GMapKey k where
  data GMap k :: Type -> Type
  insert :: GMap k v -> k -> v -> GMap k v
  lookup :: GMap k v -> k -> Maybe v
  empty  :: GMap k v

instance (GMapKey a, GMapKey b) => GMapKey (Either a b) where
  data GMap (Either a b) v = GMapEither (GMap a v) (GMap b v)
  ...method declarations...

Вот несколько списков экспорта и их значения:

  • module GMap( GMapKey )
    

    Экспортирует только имя класса.

  • module GMap( GMapKey(..) )
    

    Экспортирует класс, связанный тип GMap и функции-члены empty, lookup, и insert. Конструкторы данных GMap (в данном случае GMapEither) не экспортируются.

  • module GMap( GMapKey( type GMap, empty, lookup, insert ) )
    

    То же, что и в предыдущем пункте. Обратите внимание на ключевое слово «type».

  • module GMap( GMapKey(..), GMap(..) )
    

    То же, что и в предыдущем пункте, но также экспортирует все конструкторы данных для GMap, а именно GMapEither.

  • module GMap ( GMapKey( empty, lookup, insert), GMap(..) )
    

    То же, что и в предыдущем пункте.

  • module GMap ( GMapKey, empty, lookup, insert, GMap(..) )
    

    То же, что и в предыдущем пункте.

Два момента, на которые следует обратить внимание:

  • Вы не можете написать GMapKey(type GMap(..)) — т.е. вложенные спецификации подкомпонентов недопустимы. Чтобы указать конструкторы данных GMap нужно перечислить их отдельно.
  • Рассмотрим этот пример:

    module X where
      data family D
    
    module Y where
      import X
      data instance D Int = D1 | D2
    

    Модуль Y экспортирует все сущности, определённые в Y, а именно конструкторы данных D1 и D2, и неявным образом семейство данных D, даже если оно определено в X. Это означает, что вы можете написать import Y( D(D1,D2) ) без явного списка экспорта, как в этом примере:

         module Y( D(..) ) where ...
    or   module Y( module Y, D ) where ...
    

6.4.9.5.2. Экземпляры

Экземпляры семейств неявно экспортируются, как и экземпляры классов. Однако это относится только к заголовкам экземпляров, а не к конструкторам данных, которые они определяют.

6.4.9.6. Семейства типов и объявления экземпляров

Семейства типов требуют от нас расширить правила для формы заголовков экземпляров, которые приведены в Смягченные правила для заголовка экземпляра. В частности:

  • Семейства типов данных могут появляться в заголовке экземпляра
  • Семейства синонимов типов не могут появляться (вообще) в заголовке экземпляра

Причина этого ограничения заключается в том, что нет способа проверить соответствие экземпляров. Рассмотрим

type family F a
type instance F Bool = Int

class C a

instance C Int
instance C (F a)

Теперь ограничение (C (F Bool)) будет соответствовать обоим экземплярам. Ситуация особенно плоха, потому что экземпляр типа для F Bool может находиться в другом модуле, или даже в модуле, который ещё не написан.

Однако, экземпляры класса типов экземпляров семейств данных могут быть определены так же, как и любой другой тип данных. Например, мы можем сказать

data instance T Int = T1 Int | T2 Bool
instance Eq (T Int) where
  (T1 i) == (T1 j) = i==j
  (T2 i) == (T2 j) = i==j
  _      == _      = False

Обратите внимание, что экземпляры класса всегда предназначены для конкретных экземпляров семейства данных, а не для всего семейства в целом. Это по сути те же причины, по которым мы не можем определить функцию верхнего уровня, которая выполняет сопоставление с образцом для конструкторов данных разных экземпляров одного семейства типов. Это потребовало бы форму расширяемой конструкции case.

Объявления экземпляров данных также могут иметь deriving-строки. Например, мы можем написать

data GMap () v = GMapUnit (Maybe v)
               deriving Show

что неявно определяет экземпляр в форме

instance Show v => Show (GMap () v) where ...

6.4.9.7. Инъективные семейства типов

TypeFamilyDependencies
Подразумевает:

TypeFamilies

С момента:

8.0.1

Разрешает анотации функциональной зависимости для семейств типов. Это позволяет определять инъективные семейства типов.

Начиная с GHC 8.0, семейства типов могут быть снабжены информацией об инъективности. Эта информация затем используется GHC во время проверки типов для разрешения неоднозначностей типов в ситуациях, когда переменная типа появляется только в приложениях семейства типов. Рассмотрим этот искусственный пример:

type family Id a
type instance Id Int = Int
type instance Id Bool = Bool

id :: Id t -> Id t
id x = x

Здесь определение id будет отклонено, потому что переменная типа t появляется только в приложениях семейства типов и поэтому является неоднозначной. Но этот код будет принят, если мы скажем GHC, что Id инъективна, что означает, что будет возможно вывести t в местах вызова из типа аргумента:

type family Id a = r | r -> a

Инъективные семейства типов активируются с помощью расширения языка -XTypeFamilyDependencies. Это расширение подразумевает -XTypeFamilies.

Для получения подробной информации об инъективных семействах типов обратитесь к статье Haskell Symposium 2015 Инъективные семейства типов для Haskell.

6.4.9.7.1. Синтаксис анотации инъективности

Анотация инъективности добавляется после заголовка семейства типов и состоит из двух частей:

  • переменная типа, которая называет результат семейства типов. Синтаксис: = tyvar или = (tyvar :: kind). Переменная типа должна быть новой.
  • анотация инъективности вида | A -> B, где A — это переменная результата типа (см. предыдущий пункт), а B — список переменных типа и вида аргументов, в которых семейство типов является инъективным. Можно опустить некоторые переменные, если семейство типов не инъективно по ним.

Примеры:

type family Id a = result | result -> a where
type family F a b c = d | d -> a c b
type family G (a :: k) b c = foo | foo -> k b where

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

6.4.9.7.2. Проверка анотации инъективности по уравнениям семейства типов

После того, как пользователь объявляет семейство типов инъективным, GHC должен проверить, что это объявление корректно, т.е. что уравнения семейства типов не нарушают анотацию инъективности. Общая идея заключается в том, что если хотя бы одно уравнение (пункты (1), (2) и (3) ниже) или пара уравнений (пункты (4) и (5) ниже) нарушает анотацию инъективности, то семейство типов не инъективно в том виде, в котором заявляет пользователь, и выдаётся ошибка. В пунктах ниже RHS относится к правой части уравнения семейства типов, проверяемого на инъективность. LHS относится к аргументам этого уравнения семейства типов. Ниже приведены правила, которым следует GHC при проверке инъективности семейства типов:

  1. Если правая часть уравнения семейства типов является приложением семейства типов, GHC сообщает, что семейство типов не инъективно.
  2. Если правая часть уравнения семейства типов — это просто переменная типа, то мы требуем, чтобы все переменные LHS (включая неявные переменные вида) также были простыми. Другими словами, это должно быть единственное уравнение для этого семейства типов, и оно должно охватывать все возможные шаблоны. Если шаблоны не охватывают все варианты, GHC сообщает, что семейство типов не инъективно.
  3. Если переменная типа LHS, объявленная как инъективная, не упоминается в инъективной позиции в RHS, GHC сообщает, что семейство типов не инъективно. Инъективная позиция означает либо аргумент конструктора типа, либо инъективный аргумент семейства типов. Вывод типа может потенциально зацикливаться при поиске под инъективными семействами типов в RHS, поэтому это требует UndecidableInstances; GHC предлагает включить этот флаг, когда это необходимо.
  4. Открытые семейства типов Открытые семейства типов проверяются постепенно. Это означает, что когда модуль импортируется, экземпляры семейства типов, содержащиеся в этом модуле, проверяются по отношению к экземплярам, присутствующим в уже импортированных модулях.

    Пара уравнений открытого семейства типов проверяется путем попытки унификации их правых частей. Если правые части не унифицируются, эта пара не нарушает анотацию инъективности. Если унификация удается с подстановкой, то LHS унифицированных уравнений должны быть идентичными при этой подстановке. Если они не идентичны, GHC сообщает, что семейство типов не инъективно.

  5. В закрытом семействе типов все уравнения упорядочены и находятся в одном месте. Уравнения также проверяются попарно, но на этот раз уравнение должно быть спарено со всеми предшествующими уравнениями. Конечно, закрытое семейство типов с одним уравнением тривиально инъективно (если не выполняется (1), (2) или (3) выше).

    При проверке пары уравнений закрытого семейства типов GHC пытается унифицировать их правые части. Если они не унифицируются, эта пара уравнений не нарушает анотацию инъективности. Если правые части могут быть унифицированы под некоторой подстановкой (возможно пустой), то либо LHS унифицируются под той же подстановкой, либо LHS последнего уравнения подразумевается предыдущими уравнениями. Если ни одно из условий не выполняется, GHC сообщает, что семейство типов не инъективно.

Обратите внимание, что для целей проверки инъективности в пунктах (4) и (5) GHC использует специальную разновидность алгоритма унификации, которая обрабатывает приложения семейства типов как потенциально унифицируемые с чем угодно.

© 2002–2007 The University Court of the University of Glasgow. All rights reserved.
Licensed under the Glasgow Haskell Compiler License.
https://downloads.haskell.org/~ghc/9.12.1/docs/users_guide/exts/type_families.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API