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)
Iceland_jack
  • 6,848
  • 7
  • 37
  • 46
fata
  • 87
  • 1
  • 6

3 Answers3

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.

dfeuer
  • 48,079
  • 5
  • 63
  • 167
  • I see this as the most elegant solution until now. There is just a small typo here: `ToJSON =` should be `toJSON =`. – fata Mar 21 '19 at 15:50
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.

dfeuer
  • 48,079
  • 5
  • 63
  • 167
leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
  • 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? – fata Mar 20 '19 at 20:59
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.

danidiaz
  • 26,936
  • 4
  • 45
  • 95
  • 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. – dfeuer Mar 22 '19 at 01:51
  • @dfeuer 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. – danidiaz Mar 22 '19 at 07:24
  • 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. – dfeuer Mar 22 '19 at 07:56
  • @dfeuer 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]" https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html?#instance-resolution 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. – danidiaz Mar 22 '19 at 18:30
  • 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. – dfeuer Mar 22 '19 at 20:09
  • related twitter thread https://twitter.com/fresheyeball/status/1247991094157049857 – danidiaz Apr 09 '20 at 09:56