Deriving Instances Automatically
Let's have one more look at our implementation of the Eq
instance for our
BannerNumber
type:
newtype BannerNumber = B Int
instance Eq BannerNumber where
B x == B y = x == y
It's better than the version we had before where we defined both (==)
and
(/=)
, but it's still all boilerplate. All we're doing is to remove the data
constructor B
and then test the two wrapped integers x
and y
for equality.
Here that's not so bad, but think about implementing an equality test for
student records that implements the logic that two student records are the same
if they have all the same fields:
data StudentRecord = SR
{ bannerNumber :: BannerNumber
, name :: String
, address :: String
, transcript :: Transcript
}
instance Eq StudentRecord where
x == y = bannerNumber x == bannerNumber y
&& name x == name y
&& address x == address y
&& transcript x == transcript y
This gets tedious very quickly. Thankfully, Haskell has a mechanism to
autogenerate this type of code for us, making it necessary to provide custom
implementations of standard type classes, such as Eq
, Ord
, etc. only if we
want them to deviate from the default behaviour. Here is how we can get Eq
,
Ord
, and Show
instances for banner numbers and student records in one fell
swoop, with minimal typing:
newtype BannerNumber = B Int
deriving (Eq, Ord, Show)
data Transcript = T
deriving (Eq, Ord, Show)
data StudentRecord = SR
{ bannerNumber :: BannerNumber
, name :: String
, address :: String
, transcript :: Transcript
}
deriving (Eq, Ord, Show)
That's it. The deriving
clause at the end of a type definition tells the
compiler to auto-generate instances of the listed type classes for the type
we're defining. This doesn't work for all type classes. It is built into the
standard library for Eq
, Ord
, Enum
, Ix
, Bounded
, Read
, and Show
.
There are also mechanisms to teach the compiler to auto-derive instances of new
type classes we define, but this is beyond the scope of this book.
The standard instance for Eq
behaves exactly like the one for StudentRecord
we defined above: Two values of a given type are equal if and only if they were
constructed using the same data constructor and the arguments to the data
constructor are pairwise equal. In particular, this means that we can derive an
Eq
instance only if all argument types of data constructors are instances of
Eq
. That's why we had to derive Eq
, Ord
, and Show
instances for
Transcript
above to ensure that the deriving
clause for StudentRecord
succeeds.
Similarly, the standard instance for Ord
implements essentially lexicographic
comparison:
-
If
x
andy
were constructed using different data constructors, thenx < y
if the constructor used to buildx
comes before the constructor used to buildy
in the type definition.That's why with the definition
data Bool = False | True
, we haveFalse < True
. -
If
x
andy
were constructed using the same data constructor, then we compare the fields ofx
andy
one by one. Let's call these fieldsx1 x2 ... xk
andy1 y2 ... yk
. Ifx1 /= y1
, thencompare x y = compare x1 y1
. Ifx1 == y1
butx2 /= y2
, thencompare x y = compare x2 y2
. This continues until we find the firsti
such thatxi /= yi
. If each field ofx
is equal to the corresponding field ofy
, thencompare x y = EQ
.
Thus, once again, we can auto-derive an instance of Ord
for some type only if
all fields of this type are instances of Ord
.
We won't discuss the behaviour of the auto-derived implementations of Enum
,
Ix
, Bounded
, Read
or Show
.