Существует несколько способов выполнения программирования с типом данных с использованием фреймворка GHC.Generics. Один из способов — использование оберток Generically и Generically1 из GHC.Generics, с помощью которых можно вывести экземпляры, используя DerivingVia:
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DerivingVia #-}
import GHC.Generics
data V4 a = V4 a a a a
deriving
stock (Generic, Generic1)
deriving (Semigroup, Monoid)
via Generically (V4 a)
deriving (Functor, Applicative)
via Generically1 V4
Более старый подход использует DeriveGeneric, DefaultSignatures и DeriveAnyClass. Он выводит экземпляры, предоставляя выделенное общее реализацию в качестве части объявления класса типов. Этот раздел содержит очень краткий обзор того, как это сделать.
Поддержка общего программирования в GHC позволяет определять классы с методами, для которых не требуется пользовательская спецификация при создании экземпляров: тело метода автоматически выводится GHC. Это похоже на то, что происходит для стандартных классов, таких как Read и Show, например, но теперь для классов, определенных пользователем.
Примечание
В GHC ранее существовала реализация общих классов, как определено в статье «Derivable type classes», Ralf Hinze и Simon Peyton Jones, Haskell Workshop, Монреаль, сентябрь 2000 г., стр. 94-105. Они были удалены и заменены более общей поддержкой общего программирования.
6.19.3.1. Выведение представлений
Первое, что нам нужно, — это общие представления. Модуль GHC.Generics определяет несколько примитивных типов, которые используются для представления типов данных Haskell:
-- | Unit: used for constructors without arguments
data U1 p = U1
-- | Constants, additional parameters and recursion of kind Type
newtype K1 i c p = K1 { unK1 :: c }
-- | Meta-information (constructor names, etc.)
newtype M1 i c f p = M1 { unM1 :: f p }
-- | Sums: encode choice between constructors
infixr 5 :+:
data (:+:) f g p = L1 (f p) | R1 (g p)
-- | Products: encode multiple arguments to constructors
infixr 6 :*:
data (:*:) f g p = f p :*: g p
Классы Generic и Generic1 обеспечивают связь между пользовательскими типами данных и их внутренним представлением в виде суммы произведений:
class Generic a where -- Encode the representation of a user datatype type Rep a :: Type -> Type -- Convert from the datatype to its representation from :: a -> (Rep a) x -- Convert from the representation to the datatype to :: (Rep a) x -> a class Generic1 (f :: k -> Type) where type Rep1 f :: k -> Type from1 :: f a -> Rep1 f a to1 :: Rep1 f a -> f a
Generic1 используется для функций, которые могут быть определены только над контейнерами типов, такими как map. Обратите внимание, что Generic1 по умолчанию принимает типы вида Type -> Type, но если включено расширение PolyKinds, то он может принимать типы вида k -> Type, для любого вида k.
-
DeriveGeneric -
Разрешить автоматическое выведение экземпляров для типа класса
Generic.
Экземпляры этих классов могут быть выведены GHC с расширением DeriveGeneric и необходимы для автоматического определения общих экземпляров.
Например, пользовательский тип данных деревьев
data UserTree a = Node a (UserTree a) (UserTree a) | Leaf
в модуле Main в пакете с именем foo получит следующее представление:
instance Generic (UserTree a) where
-- Representation type
type Rep (UserTree a) =
M1 D ('MetaData "UserTree" "Main" "package-name" 'False) (
M1 C ('MetaCons "Node" 'PrefixI 'False) (
M1 S ('MetaSel 'Nothing
'NoSourceUnpackedness
'NoSourceStrictness
'DecidedLazy)
(K1 R a)
:*: M1 S ('MetaSel 'Nothing
'NoSourceUnpackedness
'NoSourceStrictness
'DecidedLazy)
(K1 R (UserTree a))
:*: M1 S ('MetaSel 'Nothing
'NoSourceUnpackedness
'NoSourceStrictness
'DecidedLazy)
(K1 R (UserTree a)))
:+: M1 C ('MetaCons "Leaf" 'PrefixI 'False) U1)
-- Conversion functions
from (Node x l r) = M1 (L1 (M1 (M1 (K1 x) :*: M1 (K1 l) :*: M1 (K1 r))))
from Leaf = M1 (R1 (M1 U1))
to (M1 (L1 (M1 (M1 (K1 x) :*: M1 (K1 l) :*: M1 (K1 r))))) = Node x l r
to (M1 (R1 (M1 U1))) = Leaf
Это представление генерируется автоматически, если к типу данных добавляется клауза deriving Generic. Также можно использовать самостоятельное выведение.
6.19.3.2. Написание общих функций
Общая функция определяется путем создания класса и предоставления экземпляров для каждого из типов представлений GHC.Generics. В качестве примера мы покажем общее сериализацию:
data Bin = O | I class GSerialize f where gput :: f a -> [Bin] instance GSerialize U1 where gput U1 = [] instance (GSerialize a, GSerialize b) => GSerialize (a :*: b) where gput (x :*: y) = gput x ++ gput y instance (GSerialize a, GSerialize b) => GSerialize (a :+: b) where gput (L1 x) = O : gput x gput (R1 x) = I : gput x instance (GSerialize a) => GSerialize (M1 i c a) where gput (M1 x) = gput x instance (Serialize a) => GSerialize (K1 i a) where gput (K1 x) = put x
Особенность: эта стратегия кодирования может быть ненадежной в разных версиях GHC. При выведении экземпляра Generic имеет право выбрать любую вложенность :+: и :*:, которые он выбирает, поэтому, если GHC выбирает (a :+: b) :+: c, то кодирование для a будет [O, O], b будет [O, I], а c будет [I]. Однако, если GHC выбирает a :+: (b :+: c), то кодирование для a будет [O], b будет [I, O], и c будет [I, I]. (На практике текущая реализация пытается создать более-менее сбалансированное вложение :+: и :*: таким образом, чтобы обход структуры типа данных от корня до определенного компонента можно было выполнить за логарифмическое, а не линейное время.)
Как правило, этот класс GSerialize не будет экспортироваться, поскольку экземпляры имеют смысл только для типов представлений.
6.19.3.3. Неунифицированные типы представлений
Семейство данных URec предоставляется для обеспечения общего программирования над типами данных с определенными неунифицированными аргументами. Существует шесть экземпляров, соответствующих общим неунифицированным типам:
data family URec a p
data instance URec (Ptr ()) p = UAddr { uAddr# :: Addr# }
data instance URec Char p = UChar { uChar# :: Char# }
data instance URec Double p = UDouble { uDouble# :: Double# }
data instance URec Int p = UInt { uInt# :: Int# }
data instance URec Float p = UFloat { uFloat# :: Float# }
data instance URec Word p = UWord { uWord# :: Word# }
Для удобства предоставляется шесть синонимов типов:
type UAddr = URec (Ptr ()) type UChar = URec Char type UDouble = URec Double type UFloat = URec Float type UInt = URec Int type UWord = URec Word
В качестве примера, эта декларация данных:
data IntHash = IntHash Int# deriving Generic
приводит к следующему экземпляру Generic:
instance 'Generic' IntHash where
type 'Rep' IntHash =
'D1' ('MetaData "IntHash" "Main" "package-name" 'False)
('C1' ('MetaCons "IntHash" 'PrefixI 'False)
('S1' ('MetaSel 'Nothing
'NoSourceUnpackedness
'NoSourceStrictness
'DecidedLazy)
'UInt'))
Пользователь может предоставить, например, экземпляр GSerialize UInt, чтобы экземпляр Serialize IntHash мог быть легко определен в терминах GSerialize.
6.19.3.4. Общие значения по умолчанию
Теперь остается определить класс «переднего плана», который доступен пользователю:
class Serialize a where put :: a -> [Bin] default put :: (Generic a, GSerialize (Rep a)) => a -> [Bin] put = gput . from
Здесь мы используем подпись по умолчанию, чтобы указать, что пользователю не нужно предоставлять реализацию для put, если для типа существует экземпляр Generic . Для типа UserTree , например, пользователь может просто написать:
instance (Serialize a) => Serialize (UserTree a)
Затем используется метод по умолчанию для put , соответствующий общему методу сериализации. Если вы используете DeriveAnyClass, тот же экземпляр генерируется путем добавления клаузы deriving Serialize к декларации типа UserTree. Более подробные примеры общих функций см. в пакете generic-deriving на Hackage.
6.19.3.5. Дополнительная информация
Для получения более подробной информации, пожалуйста, обратитесь к странице Haskell Wiki или оригинальной статье [Generics2010].
Jose Pedro Magalhaes, Atze Dijkstra, Johan Jeuring, and Andres Loeh. A generic deriving mechanism for Haskell. Proceedings of the third ACM Haskell symposium on Haskell (Haskell’2010), pp. 37-48, ACM, 2010.