Generics - создайте продукт записей, объединяющий все поля - PullRequest
0 голосов
/ 11 января 2019

На работе я использую дженерики, чтобы легко получить экземпляры для Data.Aeson.ToJSON и Database.Selda.SqlRow для моего API и типов баз данных. Это очень полезно, но мне часто хочется сочинять типы.

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

data AccountInfo = AccountInfo
    { firstName :: Text
    , lastName :: Text
    } deriving (Show, Eq, Generic)

data Account = Account
    { accountId :: Id Account
    , firstName :: Text
    , lastName :: Text
    } deriving (Show, Eq, Generic)

Я не могу просто вложить AccountInfo в Account, потому что я хочу, чтобы экземпляр Aeson был плоским (все поля на верхнем уровне), а Selda требует, чтобы он был плоским для хранения его в базе данных.

-- This won't work because the Aeson and Selda outputs aren't flat
data Account = Account
    { accountId :: Id Account
    , info :: AccountInfo
    } deriving (Show, Eq, Generic)

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

data Aggregate a b = Aggregate a b

data AccountId = AccountId
    { accountId :: Id Account
    } deriving (Show, Eq, Generic)

type Account = Aggregate AccountId AccountInfo

Я знаю, как вручную написать экземпляр ToJSON и FromJSON для такого типа, но я не знаю, как написать экземпляр SQLRow.

Вместо этого, в качестве упражнения для изучения дженериков, я хотел бы написать экземпляр Generic для Aggregate, где Aggregate AccountId AccountInfo имеет точно такое же представление, как и первое определение Account выше (со всеми тремя полями, сплющенными )

Как я могу это сделать? Я читал о дженериках в течение дня, и я довольно застрял.

1 Ответ

0 голосов
/ 11 января 2019

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

Более конкретно, утверждение «Я хотел бы написать общий экземпляр для Aggregate» не имеет смысла. Мы делаем общие представления экземплярами классов (class ToJSON), а не структур данных. И общие экземпляры JSON уже написаны ...

К счастью, хорошее решение может быть легким. Вам просто нужен способ работы с коллекциями объектов JSON. Ниже я продемонстрирую, как объединить два типа данных, которые имеют JSON.Object представлений. Поскольку JSON.Object - это просто хэш-карта, которую можно объединить с помощью union, я просто конвертирую свои значения haskell в объекты и выполняю объединение. Есть возможности для совершенствования, но это идея.

import qualified Data.Aeson as JSON
import qualified Data.Aeson.Types as JSON
import GHC.Generics
import qualified Data.HashMap.Lazy as HashMap

data A = A { fieldA :: String } deriving (Show,Eq,Generic)
data B = B { fieldB :: String } deriving (Show,Eq,Generic)

instance ToJSON A
instance ToJSON B
instance FromJSON A
instance FromJSON B

toObj :: ToJSON a => a -> Maybe JSON.Object
toObj = JSON.parseMaybe parseJSON . toJSON

toJSONAB :: A -> B -> Maybe JSON.Value
toJSONAB a b = do
   aObj <- toObj a
   bObj <- toObj b
   return . JSON.Value $ HashMap.union aObj bObj 

В этот момент вы должны вызывать toJSONAB вместо toJSON, когда вам нужно вывести JSON. Получение A или B с выхода включено. То есть

(toJSONAB a b >>= JSON.parseMaybe parseJSON) :: Maybe <Desired Type>

будет анализировать либо A, либо B, в зависимости от предоставленной сигнатуры типа (вывод).

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

data AB = AB A B

, которые обеспечивают безопасность типов для вашего кода. В конце концов, вас интересуют конкретные типы, а не специальная коллекция JSON-представлений типов. Чтобы сделать это через Generics, я предлагаю новый класс, такой как ниже (непроверенный и неполный),

class ToFlatJSON a where
  toFlatJSON :: a -> JSON.Value
  default toFlatJSON :: (Generic a, GToFlatJSON (Rep a)) => a -> JSON.Value
  toFlatJSON = gToFlatJSON . from

class GToFlatJSON a where
  gToFlatJSON :: a p -> JSON.Value

и предоставьте GToFlatJSON экземпляров для любых необходимых обобщенных представлений, найденных в GHC.Generics.

instance (ToJSON a) => GToFlatJSON (K1 i a) where
  gToFlatJSON (K1 a) = toJSON a

instance (GToFlatJSON a) => GToFlatJSON (a :*: a') where
  gToFlatJSON (a :*: a') = cmb (gToFlatJSON a) (toFlatJSON a')
    where
      cmb = someFunctionLike_toJSONAB

instance (GToFlatJSON a) => GToFlatJSON (M1 t i a) where
  gToFlatJSON (M1 _ _ a) = gToFlatJSON a
    where
      cmb = someFunctionLike_toJSONAB

Затем вы сможете определить пустые ToFlatJSON экземпляры, как вы делаете с ToJSON

instance ToFlatJSON a

и используйте toFlatJSON вместо toJSON. Вы можете определить toJSON в терминах toFlatJSON. Для каждого типа данных вам потребуется:

instance ToFlatJSON AB 
instance ToFlatJSON AB => ToJSON AB where
  toJSON = toFlatJSON 

Итак, в итоге, вы можете легко создать тип комбинации, работая с самими представлениями JSON, то есть с объединением их представлений объектов. Вы можете восстановить исходные типы напрямую, используя их fromJSON. Нет никакого способа перегрузить To/FromJSON универсальные экземпляры, но вы можете создать новый подобный класс и его универсальные экземпляры. Я лично рекомендую для этого приложения держаться подальше от дженериков. Я думаю, что пользовательский To / FromJSON для ваших типов данных будет самым прямым методом.

...