Spec-Zone.ru › Haskell 9

6.19.3. Обобщённое программирование

Существует несколько способов выполнять обобщённое программирование с учётом типов данных, используя инфраструктуру 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» Ральфа Хинце и Саймона Пейтона Джонса (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
Начиная с:

7.2.1

Статус:

Включено в GHC2024, GHC2021

Разрешает автоматический вывод экземпляров для класса типов 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].

[Generics2010]

Jose Pedro Magalhaes, Atze Dijkstra, Johan Jeuring и Andres Loeh. Механизм обобщённого вывода для Haskell. Материалы третьего симпозиума ACM по Haskell (Haskell’2010), с. 37–48, ACM, 2010.

© 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.14.1/docs/users_guide/exts/generics.html

Spec-Zone.ru

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