DRYer Generic types in Haskell

Viewed 84

Say I have the following types:

newtype AddressID = {unAddressId :: UUID } deriving (Generic, Show, Eq)
newtype PersonID = {unPersonId :: UUID } deriving (Generic, Show, Eq)
data Address = { addressId :: AddressID} deriving (Generic, Show, Eq)
data Person  = { personId :: PersonID } deriving (Generic, Show, Eq)
data AddressDto = AddressDto { addressDtoId :: !UUID } deriving (Generic, Show, Eq)

data PersonDto = PersonDto { personDtoId :: !UUID } deriving (Generic, Show, Eq)

type AddressListDto = HashMap UUID AddressDto

type PersonListDto = HashMap UUID PersonDto

instance FromJSON PersonDto where
  parseJSON = genericParseJSON $ apiOptions "personDto"

instance ToJSON PersonDto where
  toJSON     = genericToJSON $ apiOptions "personDto"
  toEncoding = genericToEncoding $ apiOptions "personDto"

instance FromJSON AddressDto where
  parseJSON = genericParseJSON $ apiOptions "addressDto"

instance ToJSON AddressDto where
  toJSON     = genericToJSON $ apiOptions "addressDto"
  toEncoding = genericToEncoding $ apiOptions "addressDto"

with the following utility functions:

fromAddress :: Address -> AddressDto
fromAddress Address{..} = AddressDto {addresDtoId = unAddressId addressId}

fromPerson :: Person -> PersonDto
fromAddress Person{..} = PersonDto {personDtoId = unPersonId personId}

appendPerson :: PersonListDto -> Person -> PersonListDto
appendPerson pld per = insert (personDtoId $ fromPerson per) (fromPolicy per) pld


fromPersonList :: [Person] -> PersonListDto
fromPersonList = foldl appendPerson empty


appendAddress :: AddressListDto -> Address -> AddressListDto
appendAddress ald addr = insert (addressDtoId $ fromAddress addr) (fromAddress addr) ald


fromAddressList :: [Address] -> AddressListDto
fromAddressList = foldl appendAddress empty

This code works just fine, however it is very repeated, and balloons with the number of these internal types. The functions are identical, but operate across different objects, with a naming convention on helper functions being the only thing separating the different implementations.

What is the Haskell approach to creating more generically reusable helper functions across these types? How do I go about creating a fromEntity, fromDto, appendEntityToDtoList and fromEntityList functions? Is there a way I can encode (the virtually identical) To/FromJSON instances without repeating myself? Are Typeclasses appropriate here? Is there some good material on how to use them for this purpose?

1 Answers

For the JSON instances, yes, you can definitely reduce the boilerplate, at the cost of some setup. The biggest trick is a relatively new GHC feature, DerivingVia. This lets you explain how to create an instance for one type in terms of the instance for another.

{-# language DeriveGeneric, DerivingVia, StandaloneDeriving
 , TypeFamilies, UndecidableInstances, ScopedTypeVariables
 , PolyKinds, TypeApplications #-}

-- A newtype wrapper for the type we want an instance for
newtype Optionish a = Optionish a

-- A FromJSON instance for Optionish
instance
  ( Generic a
  , Rep a ~ M1 i c f
  , Datatype c
  , GFromJSON Zero f
  )
  => FromJSON (Optionish a) where
  parseJSON v = Optionish <$> genericParseJSON (apiOptions (lower name)) v
    where
      name = datatypeName (Proxy3 @c)

data Proxy3 d f a = Proxy3

lower :: String -> String
lower "" = ""
lower (x:xs) = toLower x : xs

We can use this like so:

deriving via Optionish PersonDto
  instance FromJSON PersonDto

You can do just the same sort of thing for ToJSON.

Related