Можно ли перечислить имена и типы полей в типе данных записи, производном от Generic?

Я знаю, что для типов данных, производных от Data.Data, constrFields дает список имен полей. Глядя на документацию GHC.Generics, я думаю, что то же самое должно быть возможно и для Generic. (но с треском не смог понять, как это сделать сам).

В частности, я ищу две вещи:

Список всех полей записи

... в программе на Haskell. Я знал, что ason может автоматически определять представление JSON любого типа данных записи, производного от Generic, но чтение его исходного кода только подтвердило, что я здесь ничего не понимаю. Насколько я могу предположить, aeson должен быть в состоянии получить все имена полей (как Strings или ByteStrings) из типа данных записи, а также их типы (которые имеют тип что-то вроде TypeRep в Data.Typeable или экземпляр Eq: подойдет все, что можно использовать для сопоставления шаблонов блоков case).

Я смутно предполагаю, что создание класса и экземпляров для M1, :*: и т. д. - это путь, но я не смог добраться до проверки типов.

Проверка селектора записей

Чтобы получить тип данных записи, к которому она принадлежит, имя поля записи (как String) и т. д.

Например, учитывая

data Record = Record
    { recordId :: Int32
    , recordName :: ByteString
    } deriving Generic

Функция magic похожа на

typeOf (Record {}) == typeOf (magic recordId)

Возможно ли это с deriving Generic, или мне нужно прибегнуть к шаблону Haskell?


person Minoru    schedule 07.01.2015    source источник


Ответы (1)


Список всех полей записи

Это вполне возможно, и это действительно делается путем рекурсии структуры Rep с использованием класса. Приведенное ниже решение работает для типов с одним конструктором и возвращает пустые строковые имена для полей без селекторов:

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}

import Data.ByteString (ByteString)
import Data.Data
import Data.Int
import Data.Proxy
import GHC.Generics
import qualified Data.ByteString as B

data Record = Record { recordId :: Int32, recordName :: ByteString }
  deriving (Generic)

class Selectors rep where
  selectors :: Proxy rep -> [(String, TypeRep)]

instance Selectors f => Selectors (M1 D x f) where
  selectors _ = selectors (Proxy :: Proxy f)

instance Selectors f => Selectors (M1 C x f) where
  selectors _ = selectors (Proxy :: Proxy f)

instance (Selector s, Typeable t) => Selectors (M1 S s (K1 R t)) where
  selectors _ =
    [ ( selName (undefined :: M1 S s (K1 R t) ()) , typeOf (undefined :: t) ) ]

instance (Selectors a, Selectors b) => Selectors (a :*: b) where
  selectors _ = selectors (Proxy :: Proxy a) ++ selectors (Proxy :: Proxy b)

instance Selectors U1 where
  selectors _ = []

Теперь у нас может быть:

selectors (Proxy :: Proxy (Rep Record))
-- [("recordId",Int32),("recordName",ByteString)]

Наименее очевидная часть здесь — selName и Selector: этот класс можно найти в GHC.Generics, и он позволяет нам извлекать имена селекторов из сгенерированных типов селекторов. В случае Record представление

:kind! Rep Record
Rep Record :: * -> *
= D1
    Main.D1Record
    (C1
       Main.C1_0Record
       (S1 Main.S1_0_0Record (Rec0 Int32)
        :*: S1 Main.S1_0_1Record (Rec0 ByteString)))

а типы селекторов — Main.S1_0_0Record и Main.S1_0_1Record. Мы можем получить доступ к этим типам, только извлекая их из типа Rep с помощью классов или семейств типов, потому что GHC не экспортирует их. В любом случае, selName дает нам имя селектора из любого узла M1 с тегом селектора s (он имеет более общий тип t s f a -> String, но это нас здесь не касается).

Также возможно обрабатывать несколько конструкторов, и selectors возвращает [[(String, TypeRep)]]. В этом случае у нас, вероятно, было бы два класса: один, аналогичный приведенному выше, используемый для извлечения селекторов из заданного конструктора, и другой класс для сбора списков для конструкторов.

Проверка селектора записей

Тип записи легко получить из функции:

class Magic f where
  magic :: f -> TypeRep

instance Typeable a => Magic (a -> b) where
  magic _ = typeOf (undefined :: a)

Или статически:

type family Arg f where
   Arg (a -> b) = a

Однако без TH мы не можем знать, является ли функция законным селектором или просто функцией с правильным типом; они неразличимы в Haskell. Невозможно проверить имя «recordId» в magic recordId.


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

{-# language
  AllowAmbiguousTypes,
  DeriveGeneric,
  FlexibleContexts,
  FlexibleInstances,
  RankNTypes,
  TypeApplications,
  TypeInType
  #-}

import Type.Reflection
import GHC.Generics

class Selectors rep where
  selectors :: [(String, SomeTypeRep)]

instance Selectors f => Selectors (M1 D x f) where
  selectors = selectors @f

instance Selectors f => Selectors (M1 C x f) where
  selectors = selectors @f

instance (Selector s, Typeable t) => Selectors (M1 S s (K1 R t)) where
  selectors =
    [(selName (undefined :: M1 S s (K1 R t) ()) , SomeTypeRep (typeRep @t))]

instance (Selectors a, Selectors b) => Selectors (a :*: b) where
  selectors = selectors @a ++ selectors @b

instance Selectors U1 where
  selectors = []

Теперь использование становится selectors @(Rep MyType).

person András Kovács    schedule 07.01.2015
comment
Здорово! Быстрая идея заключается в том, что, поскольку recordId — это селектор, который выбирает поле по определенному индексу, можно было бы извлечь, какой это индекс, и объединить его с описанной вами функцией selectors, чтобы получить правильный имя и тип. Это невозможно? (Или это можно легко решить с помощью Template Haskell?) - person Minoru; 07.01.2015
comment
@Minoru без TH мы можем видеть только тип функции. Этого в любом случае недостаточно. (const 0 :: Record -> Int32) имеет правильный тип, но это не селектор. Что касается TH, я думаю, мы можем заключить имя функции в кавычки (например, `recordId), а затем посмотреть, где она была объявлена, чтобы увидеть, является ли она селектором. Но я не очень знаком с TH, к сожалению. - person András Kovács; 07.01.2015
comment
@ AndrásKovács Это мне очень помогло. Не могли бы вы сделать версию, которая работает и с Type.Reflection? - person John Smith; 06.08.2019
comment
@JohnSmith, я добавил это к ответу - person András Kovács; 07.08.2019