2015-02-06 71 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