Avoiding repeated instance declarations in Haskell

295 Views Asked by At

My question seems to be closely related to this one.

My code parses a yaml file, rearanges the objects and writes a new yaml file. It works perfectly well, but there is a particularly ugly part in it.

I have to declare my data structures as instances of FromJson and ToJson like this:

instance FromJSON Users where
  parseJSON = genericParseJSON (defaultOptions { fieldLabelModifier = body_noprefix })
instance ToJSON Users where
  toJSON = genericToJSON (defaultOptions { fieldLabelModifier = body_noprefix })

The problem is that I have to repeat this for 8 or so other cases:

instance FromJSON Role where
  parseJSON = genericParseJSON (defaultOptions { fieldLabelModifier = body_noprefix })
instance ToJSON Role where
  toJSON = genericToJSON (defaultOptions { fieldLabelModifier = body_noprefix })

...
...

I could not figure out how to avoid this repetition. Is there some method to declare the two functions just once (for example in a new class) and let all these data types derive from it?

Solution (see also accepted answer by dfeuer):

I personally like this solution. You'll need to add

{-# language DerivingVia #-}
{-# language UndecidableInstances #-}

newtype NP a = NP {unNP::a} 

instance (Generic a, GFromJSON Zero (Rep a)) => FromJSON (NP a) where
  parseJSON = fmap NP . genericParseJSON 
    (defaultOptions { fieldLabelModifier = body_noprefix })

instance (Generic a, GToJSON Zero (Rep a)) => ToJSON (NP a) where
  toJSON = genericToJSON (defaultOptions { fieldLabelModifier = body_noprefix }) . unNP

Then you can declare the types like this:

data User = User { ... } deriving (Show, Generic)
                         deriving FromJSON via (NP User)
                         deriving ToJSON via (NP User)
3

There are 3 best solutions below

1
On BEST ANSWER

This is what the fairly new DerivingVia extension is for, among other things.

{-# language DerivingVia #-}

newtype NP a = NP {unNP::a}

instance (Generic a, GFromJSON Zero (Rep a)) => FromJSON (NP a) where
  parseJSON = fmap NP . genericParseJSON 
    (defaultOptions { fieldLabelModifier = body_noprefix })

instance (Generic a, GToJSON Zero (Rep a)) => ToJSON (NP a) where
  toJSON = genericToJSON (defaultOptions { fieldLabelModifier = body_noprefix }) . unNP

Now, you can write

deriving via (NP User) instance FromJSON User

Or

data User = ...
  deriving Generic
  deriving (FromJSON, ToJSON) via (NP User)

and so on.

This doesn't save a lot over leftaroundabout's answer as it is. However, once you add a definition of toEncoding, it starts to look worthwhile.

Caution: I have tested none of this.

6
On

If explicitly declaring all those similar instances proves too onerous, perhaps you could parameterize your datatypes with a phantom type like

data User x = User { aa :: Int, bb :: Bool } deriving Generic

data Role x = Role { xx :: Int, dd :: Bool } deriving Generic

and then define a "marker" datatype like

data Marker

This datatype will only serve as a peg on which to hang instances like the following

{-# language UndecidableInstances #-}
instance (Generic (f Marker), GFromJSON Zero (Rep (f Marker))) => FromJSON (f Marker) where 
    parseJSON = noPrefixParseJSON

Would it be worth it? Likely not, because the definition of your datatypes becomes more complex. On the other hand, you could change aspects of the serialization by varying the marker, so you gain some flexibility.

1
On

Like,

noPrefixParseJSON :: (Generic a, GFromJSON Zero (Rep a)) => Value -> Parser a
noPrefixParseJSON
    = genericParseJSON (defaultOptions { fieldLabelModifier = body_noprefix })
noPrefixToJSON :: (Generic a, GToJSON Zero (Rep a)) => a -> Value
noPrefixToJSON
    = genericToJSON (defaultOptions { fieldLabelModifier = body_noprefix })

instance FromJSON User where parseJSON = noPrefixParseJSON
instance ToJSON User where toJSON = noPrefixToJSON
instance FromJSON Role where parseJSON = noPrefixParseJSON
instance ToJSON Role where toJSON = noPrefixToJSON
...

Sure this is still kind of repetitive, but I'd say this isn't any cause of worry any more.