Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Use MonadPlus in (.:?) to produce whatever empty value is needed instead of always producing a Nothing on failure. This way (.:?) can be re-used for lists, Maybe, or whatever other MonadPlus you desire. #59

Closed
wants to merge 1 commit into from

5 participants

@mike-burns

I've used this a few times in my libraries to produce lists or Maybes, as needed.

Here's an example (I had named it (.:<) in my code so as not to conflict with Aeson's, but have since changed that locally): https://github.com/mike-burns/github/blob/master/Github/Data.hs#L36

@mike-burns mike-burns Use MonadPlus in (.:?) to produce whatever empty value is needed inst…
…ead of always producing a Nothing on failure. This way (.:?) can be re-used for lists, Maybe, or whatever other MonadPlus you desire.
7e3e3f2
@basvandijk
Collaborator

I remember that the containers-0.2.0.0 release changed the lookup function from:

lookup :: (Monad m, Ord k) => k -> Map k a -> m a

to:

lookup :: Ord k => k -> Map k a -> Maybe a

I don't remember the rationale for that change but we may want to look it up before we make the reverse change in aeson. Maybe it had something to do with type ambiguity...

@basvandijk
Collaborator

Here's the start of the relevant thread on this change and here's its conclusion.

@mike-burns

Interesting. I do happen to love programming history.

Seems they made the change to avoid monads where fail causes a runtime error. The only way that would happen in this function is if Aeson were asked to parse a JSON value into an IO value. While that's maybe possible, I doubt someone would use (.:?) for that purpose.

I'd also be fine with something besides MonadPlus. Monoid's mempty or Alternative's empty would also satisfy the desired goal.

@hvr
hvr commented

Btw & jfyi, I've been using a more pragmatic approach since the earliest aeson versions in my code:

I have a type-class JsonDefault (inspired by the more generic Default package that should be on hackage somewhere) which provides default values for types deserializable from JSON:

class JsonDefault a where
    jdefault :: a

-- |Helper for extracting type-converted field from 'JSObject' with
-- default value fallback infered via 'JsonDefault' instance.
(.:!) :: (JsonDefault a, FromJSON a) => Object -> Text -> Parser a
obj .:! key = fromMaybe jdefault <$> obj .:? key
{-# INLINE (.:!) #-}

Besides encoding default application-specific values, I also use this to encode JSON-encoding conventions specific to our project (e.g. such that empty lists may be omitted in JSON, or bool-typed values need only be set if they result in true), e.g.:

instance JsonDefault Bool where
  jdefault = False

instance JsonDefault [a] where
  jdefault = []
@tibbe

The reason Data.Map.lookup was changed is that Maybe is general enough. Once you get a Maybe back you could always do:

case lookup k m of
    Nothing -> fail
    Just v  -> return v

I don't think MonadPlus makes sense here as there's no interesting bind going on. If it is to be changed it ought to be either Monoid or Alternative, I can't see which at this point.

@bos
Owner

I don't like @mike-burns's original pull request, sorry.

First, Maybe is the type that encodes the least amount of information possible in this situation. It's always convertible to whatever your desired type is via a simple function, e.g. maybeToList. By making the result type more general, you force every potential user of the function to wade through the world of possible result types before they can figure out how to use it.

Secondly, even if I thought it was a good idea, MonadPlus is the wrong class to be using - there's no need for a Monad constraint here. The class with least information would be Monoid, but even it drags in an irrelevant mappend method. Maybe is the right type here.

@bos bos closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Dec 29, 2011
  1. @mike-burns

    Use MonadPlus in (.:?) to produce whatever empty value is needed inst…

    mike-burns authored
    …ead of always producing a Nothing on failure. This way (.:?) can be re-used for lists, Maybe, or whatever other MonadPlus you desire.
This page is out of date. Refresh to see the latest.
Showing with 7 additions and 4 deletions.
  1. +7 −4 Data/Aeson/Types/Class.hs
View
11 Data/Aeson/Types/Class.hs
@@ -40,6 +40,7 @@ module Data.Aeson.Types.Class
) where
import Control.Applicative ((<$>), (<*>), pure)
+import Control.Monad (MonadPlus(..), mzero)
import Data.Aeson.Functions
import Data.Aeson.Types.Internal
import Data.Attoparsec.Char8 (Number(..))
@@ -735,15 +736,17 @@ obj .: key = case H.lookup key obj of
{-# INLINE (.:) #-}
-- | Retrieve the value associated with the given key of an 'Object'.
--- The result is 'Nothing' if the key is not present, or 'empty' if
--- the value cannot be converted to the desired type.
+-- If the key is not present the result is decided by 'mzero'; it is 'Nothing'
+-- when a 'Maybe' is desired, '[]' when a list is desired, and so on. The
+-- result is 'empty' if the key is present but the value cannot be converted
+-- to the desired type.
--
-- This accessor is most useful if the key and value can be absent
-- from an object without affecting its validity. If the key and
-- value are mandatory, use '(.:)' instead.
-(.:?) :: (FromJSON a) => Object -> Text -> Parser (Maybe a)
+(.:?) :: (FromJSON (m a), MonadPlus m) => Object -> Text -> Parser (m a)
obj .:? key = case H.lookup key obj of
- Nothing -> pure Nothing
+ Nothing -> pure mzero
Just v -> parseJSON v
{-# INLINE (.:?) #-}
Something went wrong with that request. Please try again.