这是代表用户的 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 ....
这经常在“你为什么需要一个 Monad”的解释中得到证明
因此,我需要编写看起来像的解析器(.:? "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 与原始源代码更兼容。
但是,仅出于理论目的,很高兴看到如何解决原始问题。