-
TypeFamilies -
- Подразумевает:
- Подразумевается расширением:
- Начиная с версии:
-
6.8.1
Разрешает использовать и определять индексированные семейства типов и данных.
Индексированные семейства типов — это расширение, предназначенное для поддержки программирования на уровне типов. Семейства типов представляют собой обобщение ассоциированных типов данных [AssocDataTypes2005] и ассоциированных синонимов типов [AssocTypeSyn2005]. Сами семейства типов описаны в работе Schrijvers 2008 года [TypeFamilies2008]. Семейства типов по существу предоставляют индексированные типами типы данных и именованные функции над типами. Они полезны для обобщённого программирования, библиотечных интерфейсов с большим количеством параметров, а также интерфейсов с расширенной статической информацией — подобно зависимым типам. Их также можно рассматривать как альтернативу функциональным зависимостям, однако они обеспечивают более функциональный стиль программирования на уровне типов, чем реляционный стиль функциональных зависимостей.
Индексированные семейства типов, или просто семейства типов, — это конструкторы типов, представляющие множества типов. Элементы множества задаются передачей конструктору семейства типов параметров типов, называемых индексами типов. Различие между обычными параметризованными конструкторами типов и конструкторами семейств во многом сходно с различием между параметрически полиморфными функциями и (ад-hoc полиморфными) методами классов типов. Параметрически полиморфные функции ведут себя одинаково для всех экземпляров типов, тогда как поведение методов классов может меняться в зависимости от параметров типа класса. Аналогично, обычные конструкторы типов предполагают одинаковое представление данных для всех экземпляров типов, а конструкторы семейств могут иметь разные типы представления для разных индексов типов.
Индексированные семейства типов бывают трёх видов: семейства данных, открытые семейства синонимов типов и закрытые семейства синонимов типов. Это индексированные варианты алгебраических типов данных и синонимов типов соответственно. Экземплярами семейств данных могут быть типы данных и newtype.
Семейства типов включаются языковым расширением TypeFamilies. Дополнительные сведения об использовании семейств типов в GHC доступны на странице Haskell Wiki о семействах типов.
«Ассоциированные типы с классом», M. Chakravarty, G. Keller, S. Peyton Jones и S. Marlow. В материалах «32-го ежегодного симпозиума ACM SIGPLAN-SIGACT по принципам языков программирования (POPL’05)», страницы 1–13, ACM Press, 2005.
«Ассоциированные синонимы типов», M. Chakravarty, G. Keller и S. Peyton Jones. В материалах «Десятой международной конференции ACM SIGPLAN по функциональному программированию», ACM Press, страницы 241–253, 2005.
«Проверка типов с открытыми функциями типов», T. Schrijvers, S. Peyton-Jones, M. Chakravarty и M. Sulzmann, в материалах «ICFP 2008: 13-й международной конференции ACM SIGPLAN по функциональному программированию», ACM Press, страницы 51–62, 2008.
6.4.10.1. Семейства данных
Семейства данных бывают двух видов: (1) их можно определить на верхнем уровне или (2) они могут находиться внутри классов типов (в этом случае они называются ассоциированными типами). Первый вариант более общий, поскольку в нём индексы типов не обязаны совпадать с параметрами класса. Однако второй вариант позволяет сделать код более чётко структурированным и получать предупреждения компилятора, если некоторые экземпляры типов были пропущены — возможно, случайно. Далее мы сначала всегда рассматриваем общий вариант объявления на верхнем уровне, а затем описываем дополнительные ограничения, накладываемые на ассоциированные типы.
6.4.10.1.1. Объявления семейств данных
Индексированные семейства данных вводятся сигнатурой, например
data family GMap k :: Type -> Type
Специальный family отличает семейство от стандартного объявления data. Аннотация результирующего рода необязательна и, как обычно, по умолчанию равна 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.10.1.2. Объявления экземпляров данных
Объявления экземпляров семейств данных и newtype очень похожи на стандартные объявления data и newtype. Различий всего два: за ключевым словом data или newtype следует instance, а некоторые или все аргументы типа могут быть не переменными, но не могут содержать типы forall или семейства синонимов типов. Однако семейства данных, как правило, разрешены в параметрах типа, а синонимы типов допустимы, если они применены ко всем аргументам и раскрываются в допустимый тип — точно так же, как это требуется для вхождений синонимов типов в параметрах экземпляров классов. Например, экземпляр 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 или 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
- В объявлении
data instanceилиnewtype instanceможно использовать предложениеderiving.
Даже если семейства данных определены как объявления верхнего уровня, функции, выполняющие разные вычисления для разных экземпляров семейства, всё ещё может потребоваться определять как методы классов типов. В частности, следующий вариант невозможен:
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
Учитывая возможности GADT (обобщённых алгебраических типов данных), может показаться, что определение, подобное приведённому выше, должно быть возможным. Однако семейства типов, в отличие от GADT, открыты: новые экземпляры всегда можно добавлять, в том числе, возможно, в других модулях. Поддержка сопоставления с образцом для разных экземпляров данных потребовала бы расширяемой конструкции case.
6.4.10.1.3. Перекрытие экземпляров данных
Объявления экземпляров семейства данных, используемого в одной программе, не должны перекрываться ни при каких обстоятельствах, независимо от того, являются они ассоциированными или нет. В отличие от экземпляров классов типов, здесь дело не только в согласованности, но и в безопасности типов.
6.4.10.2. Семейства синонимов
Семейства типов бывают трёх видов: (1) их можно объявить как открытые семейства на верхнем уровне, (2) их можно объявить как закрытые семейства на верхнем уровне или (3) они могут находиться внутри классов типов (в этом случае они называются ассоциированными синонимами типов). Семейства верхнего уровня более общие, поскольку индексы типов в них не обязаны совпадать с параметрами класса. Однако ассоциированные синонимы типов позволяют сделать код более чётко структурированным и получать предупреждения компилятора, если некоторые экземпляры типов были пропущены — возможно, случайно. Далее мы сначала всегда рассматриваем общие варианты объявлений верхнего уровня, а затем описываем дополнительные ограничения, накладываемые на ассоциированные типы. Обратите внимание, что закрытых ассоциированных синонимов типов не существует.
6.4.10.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.10.2.2. Объявления экземпляров типов
Объявления экземпляров семейств типов очень похожи на стандартные объявления синонимов типов. Различий всего два: за ключевым словом type следует instance, а некоторые или все аргументы типа могут быть не переменными, но не могут содержать типы forall или семейства синонимов типов. Однако семейства данных, как правило, разрешены, а синонимы типов допустимы, если они применены ко всем аргументам и раскрываются в допустимый тип, — это точно те же требования, что и для экземпляров данных. Например, экземпляр [e] для Elem выглядит так:
type instance Elem [e] = e
Аргументы типов можно заменять символами подчёркивания (_), если имена аргументов не имеют значения. Это равносильно записи переменных типов с уникальными именами. Неиспользуемые аргументы типов можно заменить или предварить символами подчёркивания, чтобы избежать предупреждений при включённом флаге -Wunused-type-patterns. Применяются те же правила, что и для объявлений экземпляров данных.
Как и в случае с объявлениями экземпляров данных, при включённом расширении ExplicitForAll переменные типов и родов можно явно связывать в объявлении экземпляра типа.
Объявления экземпляров семейств типов допустимы только при наличии в области видимости соответствующего объявления семейства — так же, как для экземпляров классов требуется, чтобы объявление класса было видимым. Кроме того, каждое объявление экземпляра должно соответствовать роду, заданному объявлением семейства, а число параметров типа в объявлении экземпляра должно совпадать с числом параметров типа в объявлении семейства. Наконец, правая часть экземпляра типа должна быть мономорфным типом (то есть не может содержать forall), а после раскрытия всех полностью насыщенных обычных синонимов типов в ней не должно оставаться никаких синонимов, кроме синонимов семейств.
6.4.10.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, соответствующее этому объявлению «boot», может иметь в исходном файле hs любое число уравнений (в том числе ни одного). Дополнительные сведения о взаимно рекурсивных модулях с модулями hs-boot (включая семейства типов) см. в разделе Взаимно рекурсивные модули и файлы hs-boot.
6.4.10.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.10.2.5. Совместимость и раздельность уравнений семейств типов
Для уравнений семейств типов необходимы некоторые ограничения, чтобы не допустить неоднозначной системы переписывания. Поэтому уравнения открытых семейств типов должны быть совместимы. Два шаблона типов совместимы, если:
- все соответствующие типы и неявные роды в шаблонах являются раздельными; или
- два шаблона унифицируются, порождая подстановку, и правые части равны при этой подстановке.
Два типа считаются раздельными, если при любых возможных подстановках эти типы нельзя свести к общему результату.
Первое условие «совместимости» проще для понимания. Оно означает, что шаблоны двух разных экземпляров семейства типов не могут перекрываться. Например, следующий вариант запрещён:
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.10.2.6. Разрешимость экземпляров синонимов типов
Чтобы гарантировать разрешимость вывода типов при наличии семейств типов, необходимо наложить ряд дополнительных ограничений на формирование объявлений экземпляров типов (ср. определение 5 («Ослабленные условия») из работы «Проверка типов с открытыми функциями типов»). Объявления экземпляров имеют общий вид:
type instance F t1 .. tn = t
причём для каждого применения семейства типов (G s1 .. sm) в t требуется, чтобы:
-
s1 .. smне содержали конструкторов семейств типов; - общее число символов (конструкторов типов данных и переменных типов) в
s1 .. smбыло строго меньше, чем вt1 .. tn; и - для каждой переменной типа
a,aвстречалось вs1 .. smне чаще, чем вt1 .. tn.
Эти ограничения легко проверить, и они обеспечивают завершение вывода типов. Однако их недостаточно для гарантии полноты вывода типов при наличии так называемых «зацикливающихся равенств», например a ~ [F a], где рекурсивное вхождение переменной типа находится под применением семейства типов и конструктора типа данных. Подробности см. в упомянутой выше статье.
Если компилятору передан параметр UndecidableInstances (см. раздел Неразрешимые экземпляры и зацикливающиеся суперклассы), указанные выше ограничения не применяются, и программист сам отвечает за завершение нормализации семейств типов во время вывода типов.
6.4.10.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.10.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
Сравните это с особой обработкой именованных подстановочных знаков в сигнатурах типов (Именованные подстановочные знаки).