Search code examples
haskellmetaprogrammingtypeclasstemplate-haskellderiving

Derive positional Show


Notice how T 5 shows in

> newtype T = T { getT :: Int } deriving Show
> T 5
T {getT = 5}

Is there some way to derive the positional, non-record-syntax variant of Show for a type that was declared with record syntax?

(btw T is only a simple example to explain the question, I'm looking for a general answer for any type defined with record syntax)

Some options I would be satisfied with:

  • TH generation for it provided by a library
  • A Generic based derivation (where the manual instance refers to an existing function)
  • An easy way / guide to manually implement Show instances
  • Any other idea I didn't think about

For a more complicated example I have this hand-written instance:

instance ... where
    showsPrec p (FuncType i o) =
        showParen (p > 0)
        (("FuncType " <>) . showsPrec 1 i . (" " <>) . showsPrec 1 o)

I would like the answer to be able to avoid this boilerplate.


Solution

  • Implementing Show by hand

    The default way of implementing Show requires a fair amount of boilerplate. That is taken care of by show-combinators, reducing the code needed to the bare essentials:

    instance Show ... where
      showPrec = flip (\(FuncType i o) -> showCon "FuncType" @| i @| o)
    

    I think this solution is the simplest possible. No extensions, no typeclass magic under the hood. Just plain functional programming.

    (Disclaimer: I wrote the two libraries mentioned in this post.)

    With GHC Generics

    There is a generic implementation of Show in generic-data: gshowsPrec (link to source). But it shows types declared with record syntax as records.

    Redoing the implementation

    One way of course is to copy the implementation and remove the special handling of records.

    {- 1. The usual boilerplate -}
    
    class GShow p f where
      gPrecShows :: p (ShowsPrec a) -> f a -> PrecShowS
    
    instance GShow p f => GShow p (M1 D d f) where
      gPrecShows p (M1 x) = gPrecShows p x
    
    instance (GShow p f, GShow p g) => GShow p (f :+: g) where
      gPrecShows p (L1 x) = gPrecShows p x
      gPrecShows p (R1 y) = gPrecShows p y
    
    {- 2. A simplified instance for (M1 C), that shows all constructors
          using positional syntax. The body mostly comes from the instance
          (GShowC p ('MetaCons s y 'False) f). -}
    
    instance (Constructor c, GShowFields p f) => GShow p (M1 C c f) where
      gPrecShows p x = gPrecShowsC p (conName x) (conFixity x) x
       where
        gPrecShowsC p name fixity (M1 x)
          | Infix _ fy <- fixity, k1 : k2 : ks <- fields =
            foldl' showApp (showInfix name fy k1 k2) ks
          | otherwise = foldl' showApp (showCon cname) fields
          where
            cname = case fixity of
              Prefix -> name
              Infix _ _ -> "(" ++ name ++ ")"
            fields = gPrecShowsFields p x
    

    Type surgery

    (Section named after my blogpost but this thread is a much simpler situation.)

    Another way is to transform the generic representation of our type to pretend that it's not declared using record syntax. Fortunately, the only difference is in a phantom type parameter, so that transformation can be as simple as coerce at run time.

    unsetIsRecord ::
      Coercible (f p) (UnsetIsRecord f p) => Data f p -> Data (UnsetIsRecord f) p
    unsetIsRecord = coerce
    
    -- UnsetIsRecord defined at the end
    

    The Data newtype basically creates a data type out of a generic representation (which is the inverse of what Generic does, in some sense). We can map a normally declared type to a Data type using toData :: a -> Data (Rep a) p.

    Finally, we can directly apply the gshowsPrec function from the generic-data library to the output of unsetIsRecord.

    instance Show T where
      showsPrec n = gshowsPrec n . unsetIsRecord . toData
    

    UnsetIsRecord should ideally be in generic-data, but since it's not yet there, here is a possible implementation:

    type family UnsetIsRecord (f :: * -> *) :: * -> *
    type instance UnsetIsRecord (M1 D m f) = M1 D m (UnsetIsRecord f)
    type instance UnsetIsRecord (f :+: g) = UnsetIsRecord f :+: UnsetIsRecord g
    type instance UnsetIsRecord (M1 C ('MetaCons s y _isRecord) f) = M1 C ('MetaCons s y 'False) f)