Разбор вложенного JSON со случайными целочисленными ключами с использованием aeson

Я использую библиотеку aeson для создания и анализа json-файлов для моего пользовательского типа Graph. Вот определения типов.

type Id = Int
type Edge = (Id, Id)
type Scenario = [Id]
data Point = Point Int Int
data Vertex = Vertex {-# UNPACK #-}!Id {-# UNPACK #-}!Point deriving (Show)
data Graph = Graph Id [Vertex] Scenario deriving (Show)

На самом деле я работаю с эйлеровыми и полуэйлеровыми графами, все вершины которых имеют позиции в 2D-пространстве. В двух словах Graph использует Data.Graph, но это не связано с моей проблемой. Каждый график имеет свой идентификатор, чтобы быстро идентифицировать его среди множества других.

Вот пример json-файла, содержащего информацию о моем графике:

{
    "id": 1,
    "vertices": {
        "3": {
            "y": 12,
            "x": 0
        },
        "2": {
            "y": 16,
            "x": 24
        },
        "1": {
            "y": 12,
            "x": 10
        }
    },
    "scenario": [
        1,
        2,
        3,
        1
    ]
}

Итак, вот моя реализация функции toJSON:

import qualified Data.Text                     as T

instance ToJSON Graph where
  toJSON (Graph id v s) = object [ "vertices" .= object (map vertexToPair v)
                                 , "scenario" .= s
                                 , "id" .= id
                                 ]
    where
      vertexToPair :: Vertex -> (T.Text, Value)
      vertexToPair (Vertex id (Point x y)) =
        (T.pack $ show id) .= object [ "x" .= x, "y" .= y]

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

instance FromJSON Graph where
  parseJSON (Object v) = do
    i <- parseJSON =<< v .: "id"
    vs <- parseJSON =<< v .: "vertices"
    sc <- parseJSON =<< v .: "scenario"
    maybeReturn ((buildGraph i sc) <$> (parseVertices vs 1))
      where
        parseVertices :: Value -> Int -> Maybe [Vertex]
        -- parseVertices (Object o) i = ???
        parseVertices _ _ = Just []

        buildGraph :: Int -> Scenario -> [Vertex] -> Graph
        buildGraph i sc vertices = Graph i vertices sc

        maybeReturn Nothing = mzero
        maybeReturn (Just x) = return x
  parseJSON _ = mzero

На самом деле я думал, что могу начать считать с 1 и получать вершины, пока программа будет разбирать каждые следующие i. Но это не лучший выбор, потому что минимальное vertex id не всегда равно 1, а иногда следующее vertex id отличается от текущего более чем на 1. Можно ли вообще разобрать такие данные? Во всяком случае, я остановился даже на самом простом случае этой проблемы (когда vertex ids начинается с 1 и увеличивается с помощью (+1)).

Хорошо. Вот как я могу получить максимальный и минимальный идентификатор вершины:

import qualified Data.Text.Read                as TR
import qualified Data.Foldable                 as Foldable

minID :: [Either T.Text Int] -> Int
minID = Foldable.maximum

maxID :: [Either T.Text Int] -> Int
maxID = Foldable.minimum

ids :: Object -> [Either T.Text Int]
ids o = map ((fmap fst) . TR.decimal) (M.keys o)

Все подписи не являются обобщенными, но это только пример.

Я попробую завтра еще раз решить этот простой случай проблемы. В любом случае, главный вопрос все еще требует ответа :)


person d12frosted    schedule 27.03.2014    source источник
comment
Зачем нужно знать индексы вершин перед их разбором? Не будет ли достаточно просто определить экземпляр FromJSON для вершин, которые считывают индексы?   -  person duplode    schedule 28.03.2014
comment
Ой. Виноват. Это не индекс, а ID :)   -  person d12frosted    schedule 28.03.2014
comment
В любом случае, если идентификаторы включены в данные JSON, вы сможете нормально их анализировать.   -  person duplode    schedule 28.03.2014
comment
Извините, но я не понимаю, что вы имеете в виду под нормальным? Все, что я знаю об идентификаторах вершин, это то, что они являются ключами и числами. Больше ничего. Но я нашел решение, смотрите мой ответ :)   -  person d12frosted    schedule 28.03.2014


Ответы (2)


Редактирование вашего ответа показывает, что вы поняли, как решить вашу насущную проблему. Тем не менее, вы можете сделать свой код намного понятнее, избегая большей части явных манипуляций со списками, необходимых для построения вершин. План таков:

  • Определите экземпляр FromJSON для Point;
  • Используйте его, чтобы определить экземпляр FromJSON для Vertex. Это будет похоже на экземпляр Rule в другом ответе на вопрос, на который вы ссылались, за исключением того, что, поскольку вы хотите использовать ключи объектов в качестве идентификаторов, оператор case будет выглядеть примерно так:

    case M.toList (o :: Object) of
        [(rawID, rawPoint)] -> Vertex (TR.decimal rawId) <$> parseJSON rawPoint
        _                   -> fail "Rule: unexpected format"
    
  • Наконец, я полагаю, что ваш существующий экземпляр FromJSON Graph заработает сразу же, если вы измените (предполагаемый) тип vs на [Vertex], учитывая экземпляр FromJSON a => FromJSON [a]. Поэтому parseVertices вам больше не понадобится.

Если у вас есть контроль над структурой JSON, может иметь смысл еще больше упростить ситуацию, сделав идентификаторы вершин полем наряду с x и y, удалив один уровень вложенности.

Обновление: реализация экземпляров на основе того, который вы добавили в свой ответ:

instance FromJSON Point where
  parseJSON (Object v) = liftM2 Point (v .: "x") (v .: "y")
  parseJSON _          = fail "Bad point"

instance FromJSON [Vertex] where
  parseJSON j = case j of
    (Object o) -> mapM parseVertex $ M.toList o
    _          -> fail "Bad vertices"
    where
      parseVertex (rawID, rawPoint) = do
        let eID = TR.decimal rawID
        liftM2 Vertex (either (fail "Bad vertex id") (return . fst) eID) $
          parseJSON rawPoint

instance FromJSON Graph where
  parseJSON (Object v) = do
    i <- parseJSON =<< v .: "id"
    vs <- parseJSON =<< v .: "vertices"
    sc <- parseJSON =<< v .: "scenario"
    return $ Graph i vs sc
  parseJSON _ = fail "Bad graph"

(Получить реализацию в качестве исполняемого примера)

Отличия от вашей версии:

  • Вам не нужно определять экземпляр для [Graph]; если вы определите экземпляр Graph, aeson будет автоматически обрабатывать списки (т. е. массивы JS) (обратите внимание, что файл FromJSON упоминается экземпляр FromJSON a => FromJSON [a]. К сожалению, мы не можем сделать то же самое (по крайней мере, не так просто) с [Vertex], учитывая, что идентификаторы вершин являются ключами, а не частью значений.
  • Я добавил fail случаев для ошибок сопоставления шаблонов, чтобы получать более информативные сообщения об ошибках.
  • Что касается вашего наблюдения о создании вершин из Either значений: ваше решение было довольно разумным. Я только реорганизовал его, используя either (из Data.Either), чтобы предоставить собственное сообщение об ошибке.

Стоит отметить, что код liftM2 (или liftM3 и т. д.) выглядит лучше, если написан в аппликативном стиле. Например, интересным случаем в экземпляре Point может стать:

parseJSON (Object v) = Point <$> v .: "x" <*> v .: "y"
person duplode    schedule 28.03.2014
comment
Спасибо за подробный ответ. Я буду работать над улучшением своего кода на выходных. Оставайтесь на связи :) - person d12frosted; 28.03.2014
comment
Эй, ознакомьтесь с моим обновлением ответа. Спасибо за помощь, теперь код намного чище. - person d12frosted; 30.03.2014
comment
Об изменении структуры JSON: здесь у меня нет власти :( - person d12frosted; 30.03.2014
comment
О, спасибо за ваше обновление. Жаль, что я не могу +1 дважды :). О [Graph]: мне нужно это реализовать, потому что реальная структура файла json такова: {graph1: {‹graph›}, graph12: {‹graph›}, ...}. О любом: вау, не знал об этой функции-конструкторе. Ура, я не потеряю преимущества ни того, ни другого. Насчет liftM2: да, я знаю, что это определение. :) И спасибо за создание сути :) - person d12frosted; 30.03.2014

Я только что реализовал решение для простого случая. Вот исходный код:

lookupE :: Value -> Text -> Either String Value
lookupE (Object obj) key = case H.lookup key obj of
        Nothing -> Left $ "key " ++ show key ++ " not present"
        Just v  -> Right v
loopkupE _ _             = Left $ "not an object"

(.:*) :: (FromJSON a) => Value -> [Text] -> Parser a
(.:*) value = parseJSON <=< foldM ((either fail return .) . lookupE) value

instance FromJSON Graph where
  parseJSON (Object v) = do
    i <- parseJSON =<< v .: "id"
    vs <- parseJSON =<< v .: "vertices"
    sc <- parseJSON =<< v .: "scenario"
    buildGraph i sc <$> concat <$> parseVertices vs
      where
        parseVertices v@(Object o) = parseFromTo minID maxID v
          where
            minID = unpackIndex $ Foldable.minimum ids
            maxID = unpackIndex $ Foldable.maximum ids
            unpackIndex eitherI = case eitherI of
              Right i -> i
              Left e -> error e
            ids = map ((fmap fst) . TR.decimal) (M.keys o)

        parseVertex i v = do
          p1 <- v .:* [(T.pack $ show i), "x"]
          p2 <- v .:* [(T.pack $ show i), "y"]
          return $ vertex i p1 p2

        parseFromTo i j v | i == j = return []
                          | otherwise = do
          vertex <- parseVertex i v
          liftM2 (:) (return [vertex]) (parseFromTo (i + 1) j v)

        buildGraph :: Int -> Scenario -> [Vertex] -> Graph
        buildGraph i sc vertices = Graph i vertices sc

  parseJSON _ = mzero

Функции lookupE и (.:*) взяты из Petr Pudlák ответить.

Мне не очень нравится эта реализация функции parseJSON. Но это работает в случаях, когда у моих вершин id с дельтой 1. Я знаю, что не смог извлечь значение из Foldable.minimum ids и Foldable.maximum ids, но это привело меня в монадный ад (маленький).

Итак, вот пример json-файла, после разбора которого мы получили Nothing:

{
    "id": 1,
    "vertices": {
        "3": {
            "y": 12,
            "x": 0
        },
        "2": {
            "y": 16,
            "x": 24
        },
        "1": {
            "y": 12,
            "x": 10
        }
    },
    "scenario": [
        1,
        2,
        3,
        1
    ]
}

Так что пока оставляю этот вопрос открытым.

Обновить

О, я только что увидел свою ошибку. У меня уже есть все ключи. :)

ids = map ((fmap fst) . TR.decimal) (M.keys o)

Теперь я оставляю этот вопрос открытым еще на несколько дней. Может быть, кто-то улучшит мое решение.

Обновление 2

Благодаря duplode я сделал код более понятным и читабельным.

Вот источник:

instance FromJSON Point where
  parseJSON (Object v) = liftM2 Point (v .: "x") (v .: "y")

instance FromJSON [Vertex] where
  parseJSON (Object o) = mapM parseVertex $ M.toList o
    where
      parseVertex (rawID, rawPoint) = Vertex (fromRight . (fmap fst) . TR.decimal $ rawID) <$> parseJSON rawPoint

instance FromJSON Graph where
  parseJSON (Object v) = do
    i <- parseJSON =<< v .: "id"
    vs <- parseJSON =<< v .: "vertices"
    sc <- parseJSON =<< v .: "scenario"
    return $ Graph i vs sc

instance FromJSON [Graph] where
  parseJSON (Object o) = mapM parseGraph $ M.toList o
    where
      parseGraph (_, rawGraph) = parseJSON rawGraph

И мне не нужны никакие вспомогательные функции для извлечения вложенных значений.

Кстати, я не знаю лучшего способа создать Vertex, чем Vertex (fromRight . (fmap fst) . TR.decimal $ rawID) <$> parseJSON rawPoint. liftM2 нельзя использовать, потому что второй аргумент имеет тип Either a b, а третий имеет тип Parser c. Не могу совместить :)

person d12frosted    schedule 28.03.2014
comment
Я обновил свой ответ реализацией, основанной на вашей, с небольшими изменениями. - person duplode; 30.03.2014