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)
newtype T = T Int deriving (Show)
and write record field accessor separately:getT :: T -> Int; getT (T n) = n
. – Shersh