При использовании обобщений пользователь определяет тип данных, который унаследует некоторую операцию (например, 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 для ваших типов данных будет самым прямым методом.