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
xandywere constructed using different data constructors, thenx < yif the constructor used to buildxcomes before the constructor used to buildyin the type definition.That's why with the definition
data Bool = False | True, we haveFalse < True. -
If
xandywere constructed using the same data constructor, then we compare the fields ofxandyone by one. Let's call these fieldsx1 x2 ... xkandy1 y2 ... yk. Ifx1 /= y1, thencompare x y = compare x1 y1. Ifx1 == y1butx2 /= y2, thencompare x y = compare x2 y2. This continues until we find the firstisuch thatxi /= yi. If each field ofxis 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.