2015-07-27 4 views
1

У меня есть тип данных, где один из полей представляет собой список один из n других типов данных (n мал и типы известны заранее). Я хотел бы сделать парсер JSON, но я не могу это понять. Я попытался создать класс типа Pet и сделать их обоими экземплярами, но он казался тупиком. Любая помощь будет оценена!Синтаксического гомогенного полиморфного массив JSon

В упрощенном примере - у меня есть тип Person данных, которые могут иметь список домашних животных, либо собаки или кошки - но не сочетание обоих.

Вот пример:

{-# LANGUAGE OverloadedStrings #-} 

import Control.Applicative 
import Data.Aeson 
import Data.ByteString.Lazy as L 
import Data.Aeson.Types (Parser) 
import Control.Monad (mzero) 

data Person = Person { 
    name :: String, 
    species :: String, 
    pets :: [?] -- a list of dogs OR cats 
} deriving Show 

instance FromJSON (Person a) where 
    parseJSON (Object v) = ??? 

data Dog = Dog { 
    dogField :: String 
} deriving Show 

instance FromJSON Dog where 
    parseJSON (Object v) = Dog <$> 
    v .: "dogField" 

data Cat = Cat { 
    catField :: String 
} deriving Show 

instance FromJSON Cat where 
    parseJSON (Object v) = Cat <$> 
    v .: "catField" 
+0

Необходимо ли ваши требования, что 'pets' не имеет кошек и собак в то же время, или это просто упрощающее предположение? – duplode

+0

Возможно, это не существенно, но оно отражает мою текущую модель данных. Но решение без этого предположения также было бы здорово! – sportanova

+0

Если вы действительно хотите разделить кошек и собак, естественным делом является наличие отдельных списков разных типов в «Человеке». – duplode

ответ

2

Стандартный способ представления либо одного типа или другого, чтобы использовать Either тип, например:

data Person { ..., pets :: Either [Dog] [Cat] } 

Кроме того, вы можете быть заинтересованы в использовании GHC Дженерики для автоматического вывода экземпляров To/FromJSON.

Пример со структурой данных, которая использует Either:

{-# LANGUAGE DeriveGeneriC#-} 

import Data.Aeson 
import GHC.Generics 

data Person = Person { 
    name :: String, 
    species :: String, 
    pets :: Either [String] [Int] 
} deriving (Show,Generic) 

instance ToJSON Person -- instances are auto-derived  
instance FromJSON Person 

doit = do 
    let me = Person "user5402" "Human" (Right [42]) 
    print $ encode me 

Если у вас есть более чем две альтернативы вы можете легко создать свой собственный тип суммы, как это:

-- assume the possible pet types are: Dog, Cat, Rodent, Reptile 
data Pets = Dogs [Dog] | Cats [Cat] | Rodents [Rodent] | Reptiles [Reptile] 
    deriving (Show, Generic) 

data Person { ..., pets :: Pets } 
    deriving (Show, Generic) 

doit = do 
    let me = Person "user5402" "Human" (Rodents [agerbil, amouse]) 
    print $ encode me 

где agerbil и amouse являются значениями Rodent.

+0

@sportanova: Вы описали своих кошек и собак как «один из n других типов данных (n мало, и типы известны заранее) ». В таких случаях вам почти никогда не нужно создавать класс, и тип суммы (то есть тип с несколькими конструкторами) часто является самой простой альтернативой. «Либо» - стандартный способ создания типа суммы для двух возможностей. – duplode

+0

@ duplode - хорошая точка; добавлен пример пользовательского типа суммы. – ErikR

+0

user5402 @duplode спасибо! Я закончил использование измененной версии вашего ответа, чтобы удалить поля «content» и «tag» из Generic ToJSON/FromJSON - см. Ниже – sportanova

0

Я изменяю ответ @ user5402, потому что мне не нравятся поля «тег» и «содержимое», которые добавили Generics. Кроме того, принимая свой ответ, так как он дал мне ключ представление о том, как структурировать тип суммы

instance FromJSON Pets where 
    parseJSON (Object o) = (parsePets o "pets") 
    parseJSON _ = mzero 

parsePets :: Object -> T.Text -> Parser Pets 
parsePets o key = case H.lookup key o of 
       Nothing -> fail $ "key " ++ show key ++ " not present" 
       Just v -> parseToCatsOrDogs (o .: "species") v 
{-# INLINE parsePets #-} 

parseToCatsOrDogs :: Parser String -> Value -> Parser Pets 
parseToCatsOrDogs speciesParser (Array v) = speciesParser >>= \species -> case species of 
    "dog" -> (V.mapM (\x -> parseJSON $ x) v) >>= \ dogVector -> return $ Dogs (V.toList dogVector) 
    "cat" -> (V.mapM (\x -> parseJSON $ x) v) >>= \ catVector -> return $ Cats (V.toList catVector) 
    _ -> mzero 
parseToCatsOrDogs _ _ = mzero 
Смежные вопросы