2015-02-06 3 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 должна быть «разворачивал».

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

[Update]: Я решил, что самое лучшее решение актуальной проблемы, было бы сохранить первоначальную структуру 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 join that result and get Parser (Maybe Object) `.

Однако было бы неплохо сделать его более простым в использовании. Я бы взял этого оператора из ответа @ 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

+0

Также см. Мое обновление выше. О более правильном решении, которое я взял. – zudov

+0

@zudov, очевидно, используя вложенную структуру данных, устраняет эту проблему, но у меня, конечно, есть случаи, когда это не так практично. Для случая, когда вам нужно пересечь дерево, которое может или не может быть, оператор, подобный приведенному выше, очень полезен. Может быть какой-то комбинатор доступен из библиотеки объективов для эзона, который также может быть использован здесь. – bheklilr