Deriving instances with a twist
When defining new data types, instance derivation can generate basic
functionality for free. However, that mechanism cannot handle all types.
For example, deriving Eq
or Show
(using the stock
strategy)
assumes that all constructor fields are instances of Eq
and Show
.
But if that condition is only broken by one field among many others, it would be a waste if the work done by deriving could not be reused.
Extensions and imports for this Literate Haskell file
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
import Data.Coerce (coerce)
Problem example
Here is a Thing
with four fields, one of which is a function. Let’s try to
derive Show
for it.
data Thing = Thing Int String Bool (Int -> Int)
deriving Show
The compiler generates the following code to recursively show each field and put the results together, inserting parentheses where necessary.
instance Show Thing where
showsPrec a_a1ar (Thing b1_a1as b2_a1at b3_a1au b4_a1av)
= showParen
a_a1ar >= 11)
(.)
((showString "Thing ")
(.)
((showsPrec 11 b1_a1as)
(.)
((showSpace
.)
((showsPrec 11 b2_a1at)
(.)
((showSpace
.)
((showsPrec 11 b3_a1au)
(.) showSpace (showsPrec 11 b4_a1av)))))))) ((
That code is then typechecked, and that fails because b4_a1av
is a function,
of type Int -> Int
, and there is no Show
instance for that type.
A Show
instance for functions
An obvious workaround is to add a dummy instance for functions, that produces some placeholder.
instance Show (a -> b) where
show _ = "_"
In fact, there is such an instance defined in base
for this purpose, in
Text.Show.Functions
,
so we could just import it.
We can use the empty import list to indicate that nothing apart from the
instance comes from this module.
import Text.Show.Functions ()
Adding such an instance makes more Show
instances derivable. However, dummy
instances for functions can make some common mistakes harder to detect at
compile time, such as forgetting to apply a function to an argument.
Furthermore, instances are always reexported, so if we’re writing a library, this also pollutes the environment of users with that controversial instance.
Parameterizing types
A better solution is to hide the problematic field from the deriving mechanism.
We start by replacing the field type with a new parameter. (Thing0
is a new
name for Thing
to avoid conflicts in this Literate Haskell file.)
data Thing0_ x = Thing0 Int String Bool x
type Thing0 = Thing0_ (Int -> Int)
The goal is to be able to apply show
to Thing0
, without manually
implementing Show
for it (which is especially desirable if Thing0
is a big
type).
First, we can standalone-derive a Show
instance for a specialization of
Thing0_
at a type with a Show
instance. We will then use that
as the basis for the actual instance.
The INCOHERENT
pragma makes it so that this instance can be ignored by the
compiler later, avoiding instance overlap errors. It is fine to use this pragma
here because the final instance will behave identically anyway. The only
purpose of this first instance is to get code derived by the compiler
somewhere accessible.
deriving instance {-# INCOHERENT #-} Show (Thing0_ (Opaque x))
where Opaque
is the following newtype
with a dummy instance that ignores
its contents:
newtype Opaque a = Opaque a
instance Show (Opaque a) where
show _ = "_"
Now we can write the actual Show
instance by “coercing” the derived one above.
Specializing showsPrec
to Thing0_ (Opaque x)
cause the above instance to be
chosen rather than the one we’re defining Thing0_ x
(even though instance
resolution can be recursive like that, e.g., Eq
for []
) because the above
one is the most specific one that matches Thing (Opaque x)
.
instance Show (Thing0_ x) where
showsPrec = coerce (showsPrec :: Int -> Thing0_ (Opaque x) -> ShowS)
Here we go.
main :: IO ()
main = print (Thing0 1 "two" True (+ 4) :: Thing0)
-- Output: Thing0 1 "two" True _
Ew?
INCOHERENT
instances are always dirty hacks, because instance resolution can
easily become unpredicable when they are abused.1 We used this pragma to
“hide” an instance created using deriving
, usually the stock
instances
for a few standard type classes. This trick also works for anyclass
deriving
of instances having default implementations that are otherwise not exported.
But once we have a “default implementation” of some form, we can modify it to
work with an altered representation of a newly defined type using more common
methods (coerce
being the cheapest one).
For this reason, libraries should export default implementations of type class
methods as separate functions, even if they use DefaultSignatures
.
For example, we can use the same technique to derive a tweaked Show
instance
from a GHC.Generics
implementation (from my package
generic-data2,
which also defines the Opaque
wrapper):
{-# LANGUAGE DeriveGeneric, ScopedTypeVariables #-}
import Data.Coerce (coerce)
import GHC.Generics (Generic)
import Generic.Data (Opaque(..), gshowsPrec) -- generic-data
data Thing_ x = Thing Int String Bool x
deriving Generic
type Thing = Thing_ (Int -> Int)
instance Show (Thing_ x) where
showsPrec = coerce (gshowsPrec :: Int -> Thing_ (Opaque x) -> ShowS)
main = print (Thing 1 "2" True (+ 4) :: Thing)
aeson uses
INCOHERENT
instances to implement theomitNothingFields
option. One possibly surprising consequence is that, for a parameterizedRecord
type, having derived an instanceforall a. ToJSON a => ToJSON (Record a)
withomitNothingFields=True
, using that instance witha = Maybe ()
will not allow fields originally of typea
to be omitted.↩︎See also my previous post An old and new library for generic deriving.↩︎