2015-02-06 84 views
2

这里是JSON对象的一部分,它表示用户:撰写可选埃宋解析器

{ "image": { "url": "http://example.com" } } 

我需要将其解析为User类型:

data User = User { imgUrl :: Maybe Text } 

天真溶液:

parseJSON (Object o) = User <$> getImgUrl o 
    where getImgUrl o = (o .:? "image") >>= maybe (return Nothing) (.:? "url") 

但是这并不比这些链条好:

case f m1 of 
    Nothing -> Nothing 
    Just m2 -> case f2 m2 of 
     Nothing -> Nothing 
     Just m3 -> case f3 m3 .... 

,常常在展示“为什么你需要一个单子”解释

因此,我需要编写解析器看起来像(.:? "url") :: Parser (Maybe a)

我试图描述与comp功能成分:

getImgUrl :: Object -> Parser (Maybe Text) 
getImgUrl o = o .:? "image" >>= comp (o .:? "url") 

comp :: (Monad m) => (a -> m (Maybe b)) -> Maybe a -> m (Maybe b) 
comp p Nothing = return Nothing 
comp p (Just o) = p o 

闻起来像一个函子,但fmap没有帮助我。

然后我决定,即组成必须继续下去:

getImgUrl :: Object -> Parser (Maybe Text) 
getImgUrl = comp2 (.:? "image") (.:? "url") o 

-- Maybe should be changed to a matching typeclass 
comp2 :: (Monad m) => (a -> m (Maybe b)) -> (b -> m (Maybe c)) -> a -> m (Maybe c) 
comp2 = undefined 

Hoogle搜索并没有帮助我,而是通过Control.Monad文档撇给了我Kliesli组成,这我不与经验。我看到一些相似性:

(>=>) :: Monad m => (a -> m b) -> (b -> m c)  -> a -> m c 
comp2 :: Monad m => (a -> m (f b)) -> (b -> m (f c)) -> a -> m (f c) 

不同的是,该组合物Maybe期间应该“解开”。

看来我接近解决方案,但仍无法找到它。请给我一些见解。

[更新]: 我已经决定,到实际问题的最佳解决方案,将是保持原有的JSON结构,并有一个嵌套的用户类型:

data User = User { image :: Maybe Image } 
data Image = Image { url :: Text } 

这完全消除我的问题,并使API与原始源更兼容。

但是,仅仅出于理论目的,很高兴看到如何解决原始问题。

回答

3

我被指出一个很好的解决方案

首先,这里是我们如何做到这一点。

parseJSON (Object o) = User . join <$> (traverse (.:? "url") =<< (o .:? "image")) 

在这里,我们得到Parser (Maybe Object)并把它传递到下一个单子的行动,这与Maybe Object工作。在traverse的帮助下,我们执行操作,如果它是Just。在结果中我们得到Parser (Maybe (Maybe Object)). What's left is to加入that result and get解析器(也许对象)`。

但是,使用它会更好。我会从@ bheklilr的回答中接受这个运算符,并将采用它来解决此问题。

-- The type can be much more generic, but for simplicity I would keep it in domain of the problem 
(.:?>) :: FromJSON a => Parser (Maybe Object) -> Text -> Parser (Maybe a) 
maybeParser .:?> key = fmap join . traverse (.:? key) =<< maybeParser 

然后我们可以使用该运算符来解析可选字段的长链。

getImgUrl :: A.Object -> Parser (Maybe Text) 
getImgUrl o = o .:? "image" .:?> "url" .:?> "foo" .:?> "bar" 

从实际的角度来看,这种解决方案并不比@ bheklilr的解决方案,我最初的“天真”的代码示例有用得多。然而,我更喜欢它,因为它不是匹配Just/Nothing它可以转换许多其他类型(例如Either

2

我能够做出过一个相对简单的组合子根据您>>= maybe (return Nothing) (.:? key)模式应该大大简化你想要做什么:

(/?) :: FromJSON a => Parser (Maybe Object) -> Text -> Parser (Maybe a) 
maybeParser /? key = maybeParser >>= maybe (return Nothing) (.:? key) 

这可以用来链一起水平任意数量的通过JSON文档:

instance FromJSON User where 
    parseJSON (Object o) = User <$> o .:? "image" /? "url" 
    parseJSON _ = mzero 

> decode "{\"image\": {\"url\": \"foobarbaz\"}}" :: Maybe User 
Just (User {imgUrl = Just "foobarbaz"}) 

又如:

data Test = Test (Maybe Int) deriving (Eq, Show) 

instance FromJSON Test where 
    parseJSON (Object o) = Test <$> o .:? "foo" /? "bar" /? "baz" /? "qux" 
    parseJSON _ = mzero 

> decode "{\"foo\": {\"bar\": {\"baz\": {\"qux\": 123}}}}" :: Maybe Test 
Just (Test (Just 123)) 

这可能不是你正在寻找的东西,但我认为它解决了你眼前的问题。考虑到它是一个复杂度非常低的1行函数,它对我来说似乎非常习惯。我认为在这种情况下真的不需要更高级别的抽象。

+0

这似乎是对于实际问题的合理解决方案。但是,看到一些令人兴奋的通用组合器会更加出色。如果没有人会在合理的时间内提出这样的建议,我会标记你的答案。 – zudov 2015-02-06 16:14:26

+0

另外,请参阅上面的我的更新。关于我采取的更正确的解决方案。 – zudov 2015-02-06 16:15:24

+0

@zudov显然使用嵌套的数据结构消除了这个问题,但我肯定有实例,这是不实际的。对于需要遍历可能存在或不存在的树的情况,像上面那样的运算符是非常有用的。在镜头库中可能有一些combinator for aeson,在这里也可以使用。 – bheklilr 2015-02-06 16:19:12