Search code examples
haskellinstancedryderivingderivingvia

Avoiding repeated instance declarations in Haskell


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)

Solution

  • 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.