Avoiding repeated instance declarations in Haskell
Asked Answered
L

3

5

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)
Lauren answered 20/3, 2019 at 19:18 Comment(0)
S
5

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.

Synthiasyntonic answered 21/3, 2019 at 2:14 Comment(1)
I see this as the most elegant solution until now. There is just a small typo here: ToJSON = should be toJSON =.Lauren
P
2

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.

Percival answered 20/3, 2019 at 19:38 Comment(1)
It's a good idea to extract the function definitions but the main problem still remains in my opinion. Maybe a meta programming solution via template haskell is the correct way to go?Lauren
E
0

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.

Engram answered 20/3, 2019 at 22:13 Comment(6)
That instance overlaps a lot. I think a newtype taking a phantom marker (or even a list of markers) would be a lot cleaner. That could also be useful for the DerivingVia approach I outlined in my answer, if you want to do a lot of type-level work up front.Synthiasyntonic
@Synthiasyntonic I believe it overlaps with more concrete types also parametrized with Marker, but the idea is that Marker will be used only with entities defined in the same module, and for those we wanted "uniform" instances anyway. A problem I see with the newtype is that if Role is nested within User, we also have to put the newtype there, which seems inconvenient.Engram
I'm pretty sure it will also affect instance resolution in some other circumstances. For example, suppose you want the instance for Const Int a, where a is neither specified nor inferred. Having your instance in scope will cause resolution to fail where it would otherwise succeed, because the compiler now wants to know that a is not the marker type.Synthiasyntonic
@Synthiasyntonic The GHC User Guide states that "It is fine for there to be a potential of overlap […] an error is only reported if a particular constraint matches more than one [instance]" downloads.haskell.org/~ghc/latest/docs/html/users_guide/… So, as long as we don't require FromJSON from a Const Int Marker in our program, we won't get any overlapping instances error.Engram
Huh .... It looks like I was overly optimistic about resolution without that instance. I'm pretty sure I've encountered situations where that hurt when messing around with Data.Constraint.Forall; I'll have to dig a bit further and see if I can produce a real example.Synthiasyntonic
related twitter thread twitter.com/fresheyeball/status/1247991094157049857Engram

© 2022 - 2024 — McMap. All rights reserved.