1

我正在使用aeson库来为我的自定义Graph类型生成和解析 json 文件。以下是类型定义。

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)

实际上,我正在使用欧拉图和半欧拉图,它们的所有顶点在二维空间中都有位置。简而言之,Graph 使用 Data.Graph,但这与我的问题无关。每个图表都有自己的 ID,以便在许多其他图表中快速识别它。

这是 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-file 解析回来时遇到了问题。主要问题是这样一个事实,我们不知道有多少顶点有特定的 Graph,所以它不能被硬编码。这是我第一次尝试编写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并获取顶点,而程序仍然会解析每个 next i。但这不是一个好的选择,因为 minimumvertex id并不总是如此1,有时 nextvertex id与 current 的差异更大1。甚至可以解析这些数据吗?无论如何,即使是这个问题的最简单情况(当vertex ids从开始1并使用 递增时(+1))我也坚持了下来。

好吧。这就是我如何获得最大和最小顶点ID:

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)

所有签名都不是通用的,但这只是示例。

明天我将再次尝试解决这个简单的问题案例。无论如何,主要问题仍然需要一个答案:)

4

2 回答 2

1

对您的答案的编辑表明您了解如何解决当前的问题。不过,您可以通过避免构建顶点所需的大多数显式列表操作来使您的代码更清晰。计划是:

  • 为;定义一个FromJSON实例Point
  • 用它来定义一个FromJSON实例Vertex。这更像是您链接到的问题的其他答案Rule中的实例,除了因为您想使用对象键作为 ID,那里的语句将变为: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 结构,那么通过将顶点 ID 设置为和 旁边的字段来进一步简化事情可能是有意义的,xy删除了一层嵌套。

更新:实例的实现,基于您添加到答案中的实例:

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],因为顶点 ID 是键而不是值的一部分。
  • 我添加fail了模式匹配失败的案例,以获得更多信息性错误消息。
  • 根据您对从Either值创建顶点的观察:您的解决方案非常合理。我只使用either(from Data.Either) 对其进行了重构,以提供自定义错误消息。

值得一提的是,如果使用 applicative 风格编写liftM2(或liftM3等)代码,看起来会更好。例如,实例中有趣的Point情况可能会变成:

parseJSON (Object v) = Point <$> v .: "x" <*> v .: "y"
于 2014-03-28T12:53:43.457 回答
0

我刚刚为简单的案例实施了解决方案。这是源代码:

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 为 delta 1 的情况。我知道我无法从Foldable.minimum idsand中提取值Foldable.maximum ids,但它把我带到了 monad 地狱(一点点)。

所以这里是一个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不能使用,因为第二个参数有 type Either a b,但第三个有 type Parser c。不能合并:)

于 2014-03-28T07:29:45.763 回答