aeson icon indicating copy to clipboard operation
aeson copied to clipboard

Support for datatypes with no constructors in TH and generics

Open evgeny-osipenko opened this issue 3 years ago • 3 comments

This PR adds support for datatypes with no constructors in TH generators (mkToJSON, mkToEncoding and mkParseJSON) and generic instances (GToJSON', GFromJSON').

For example, this PR enables this code:

data NoConstructors
    deriving (Generic)
instance ToJSON NoConstructors
instance FromJSON NoConstructors

data MyVoid
deriveJSON defaultOptions ''MyVoid

On the current master, both variants fail: the generics are missing instances of GToJSON' Encoding Zero V1 and GFromJSON' Zero V1, and TH makes an explicit call to error.

The actual behavior that is added:

  • toJSON x = seq x (error "case: V1")
  • toEncoding x = seq x (error "case: V1")
  • parseJSON _ = fail "Attempted to parse empty type"

The main use case is to allow substituting such types into polymorphic sums, where some variants are not used for a particular instantiation, and we need to serialize/deserialize the sum; though, I think it's a natural generalization by itself. As an example of such use case:

class
    ( ToJSON (ResultType lang)
    , ToJSON (FailureType lang)
    , FromJSON (ResultType lang)
    , FromJSON (FailureType lang)
    ) =>
    RecordableResult (lang :: k)
  where
    data ResultType lang
    data FailureType lang

data RequestOutcome lang
    = OutcomeSuccess (ResultType lang)
    | OutcomeFailure (FailureType lang)
    deriving (Generic)
instance (RecordableResult lang) => ToJSON (RequestOutcome lang)
instance (RecordableResult lang) => FromJSON (RequestOutcome lang)

instance RecordableResult MonadDatabase where
    data ResultType MonadDatabase = DatabaseResult Aeson.Value
        deriving (Generic, ToJSON, FromJSON)
    data FailureType MonadDatabase = DatabaseError DatabaseException
        deriving (Generic, ToJSON, FromJSON)

instance (ToJSON r, FromJSON r) => RecordableResult (MonadReader r) where
    data ResultType (MonadReader r) = ReaderResult r
        deriving (Generic, ToJSON, FromJSON)
    data FailureType (MonadReader r) -- MonadReader never fails
        deriving (Generic, ToJSON, FromJSON)
        -- but we still need ToJSON/FromJSON for the surrounding machinery to work

evgeny-osipenko avatar Jun 10 '22 17:06 evgeny-osipenko

The tests fail to compile on GHC <= 8.2.2, because this declaration:

https://github.com/haskell/aeson/blob/3de29616ba0d468ef520c1b3621b390e6a10dfe7/tests/Types.hs#L51-L52

uses EmptyDataDeriving, which is available only since GHC 8.4.

This test only asserts the successful compilation of the TH and Generic implementations, and doesn't make any checks for runtime behavior.

We can use #if __GLASGOW_HASKELL__ >= 804 to change this test for older GHCs, though I'm not sure how - should we replace the derived instances with manual ones (and how we'd do that for Typeable?), or it's fine to entirely disable this test?

evgeny-osipenko avatar Oct 12 '22 11:10 evgeny-osipenko

Typeable is always derived even if deriving Typeable is not there. deriving Typeable is just allowed for backwards compatibility. Do you actually need Show, Eq and Data? What happens if you just remove the deriving clause for NoConstructors?

Lysxia avatar Oct 12 '22 12:10 Lysxia

None of the instances were actually used, so I just removed them.

evgeny-osipenko avatar Oct 12 '22 18:10 evgeny-osipenko

Merged squashed in https://github.com/haskell/aeson/pull/971 (with some additions). Thanks!

phadej avatar Oct 17 '22 16:10 phadej