I want to create an ADT that is parameterized on a certain type family, which can be used to change the type of the fields. I'd like to derive Show
/Eq
/etc. for this type without lots of boilerplate.
Standalone deriving works, if you manually write out the constraints. I understand from questions like this one why a normal deriving Show
statement doesn't work.
But, writing out the constraints is cumbersome if you have lots of fields like this. My question is, given all the new deriving features in GHC lately (strategies, via, etc.) is there some way to do this concisely?
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE TypeFamilies #-}
module Test where
import Data.Kind
import Prelude
type family Switchable (f :: Type -> Type) x where
Switchable TextName x = String
Switchable Same x = x
data TextName x
data Same x
data Foo f = Foo {
fooField :: Switchable f Int
}
-- The constraint here is required, which gets cumbersome when many fields are present
deriving instance (Show (Switchable f Int)) => Show (Foo f)
CodePudding user response:
At first I thought you might be able to get away with a quantified constraint like this:
deriving instance (forall a. Show a => Show (Switchable f a)) => Show (Foo f)
But sadly, type families cannot appear in a quantified constraint, for the same reason they can't appear in instance heads.
So I came up with a slightly worse, but still less cumbersome solution: extract all the constraint boilerplate as yet another type family returning a constraint.
type family ShowAll f (xs :: [Type]) :: Constraint where
ShowAll f '[] = ()
ShowAll f (x:xs) = (Show (Switchable f x), ShowAll f xs)
Now you can use this to assert Show (Switchable f a)
for all interesting a
:
data Foo f = Foo {
fooField1 :: Switchable f Int,
fooField2 :: Switchable f String,
fooField3 :: Switchable f Bool
}
deriving instance ShowAll f '[Int, String, Bool] => Show (Foo f)
You still have to enumerate all the a
explicitly, but at least now it's cleaner without all the noise.
I hope this is helpful.
CodePudding user response:
There are a few different methods for this.
Make an alias for applying a constraint to all the field types, to abbreviate constraints on all of them. (Some will be redundant.)
{-# Language ConstraintKinds #-} data Foo f = Foo { fooField1 :: Switchable f Int , fooField2 :: Switchable f String , fooField3 :: Switchable f Bool } type Fields (c :: Type -> Constraint) (f :: Type -> Type) = ( c (Switchable f Int) , c (Switchable f String) , c (Switchable f Bool) ) deriving instance (Fields Show f) => Show (Foo f)
Replace the type family with a GADT.
data Foo f = Foo { fooField1 :: Field f Int , fooField2 :: Field f String , fooField3 :: Field f Bool } deriving instance Show (Foo f) data Field (f :: Type -> Type) x where Field :: Switch f x r -> Field f x deriving instance (Show x) => Show (Field f x) data Switch (f :: Type -> Type) x r where SwitchTextName :: String -> Switch TextName x String SwitchSame :: x -> Switch Same x x deriving instance (Show x) => Show (Switch f x r)
This is a 1-for-1 translation, but it can be simplified depending on the specifics, such as using only one type parameter, instead of having both
x
for the provided type andr
for the resulting representation type.Since constraints are only necessary to talk about types generically, avoid constraints by deriving instances for concrete combinations of types whenever possible.
deriving instance Show (Foo TextName) deriving instance Show (Foo Same)
Make each field type that may vary into a type parameter, then write convenience aliases for combinations of parameters.
data Foo i s b = Foo { fooField1 :: i , fooField2 :: s , fooField3 :: b } type FooByName = Foo String String String type FooByNumbers = Foo Int String Bool
#4 can be a little unwieldy with a large number of parameters, but it has the distinct advantage that you can easily write polymorphic functions to update only one parameter and ignore the others. When all the field types are indexed by a single type parameter, like in the type-family version, you may need to traverse the whole data structure to update any of them.