1
votes

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.

1
Looks like the easiest solution for you is to define data type in non-record form like newtype T = T Int deriving (Show) and write record field accessor separately: getT :: T -> Int; getT (T n) = n.Shersh

1 Answers

3
votes

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)