Home > Back-end >  Deriving Show when a type family is present (without writing manual constraints)
Deriving Show when a type family is present (without writing manual constraints)

Time:12-03

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.

  1. 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)
    
  2. 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 and r for the resulting representation type.

  3. 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)
    
  4. 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.

  • Related