Default Function Implementations
The first thing you probably noticed is that our implementation of Eq
for
BannerNumber
was a bit redundant:
instance Eq BannerNumber where
B x == B y = x == y
B x /= X y = x /= y
Clearly, B x == B y
is True
if and only if B x /= B y
is False
. We
really shouldn't have to implement both (==)
and (/=)
. When defining type
classes, we can provide default implementations of the functions in these type
classes, just as we can provide default implementations of methods in Java
interfaces and default implementations of functions in traits in Rust or Go:
class Eq a where
(==) :: a -> a -> Bool
x == y = not (x /= y)
(/=) :: a -> a -> Bool
x /= y = not (x == y)
The default implementation of (==)
is as the negation of (/=)
, and vice versa.
We can now make BannerNumber
an instance of Eq
by defining only (==)
:
instance Eq BannerNumber where
B x == B y = x == y
This replaces the default implementation of (==)
with the one given here that
is specific to BannerNumber
. For inequality testing, BannerNumber
uses the
default implementation because we did not provide a specific implementation as
part of the instance definition.
The two default implementations of (==)
and (/=)
in their current form create
a problem. Since default implementations for both methods are provided, the
compiler won't complain if we use the default implementations for both (==)
and (/=)
, not overriding any of them:
instance Eq BannerNumber
If we now try to compare two banner numbers, we run into trouble:
>>> :{
| class Eq a where
| (==) :: a -> a -> Bool
| x == y = not (x /= y)
|
| (/=) :: a -> a -> Bool
| x /= y = not (x == y)
| :}
>>> newtype BannerNumber = B Int
>>> instance Eq BannerNumber
>>> B 1 == B 2
^CInterrupted.
The reason should be obvious. To decide whether B 1 == B 2
, the default
implementation calls B 1 /= B 2
and negates the result. To decide whether
B 1 /= B 2
, the default implementation calls B 1 == B 2
and negates the
result. To decide whether B 1 == B 2
, ..., we have ourselves a nice infinite
recursion.
To break this recursion, we need to provide a custom implementation of at least
one of (==)
and (/=)
. The Haskell compiler allows us to annotate the class
definition to enforce that every instance definition does exactly this. Here is
what this looks like for Eq
:
class Eq a where
{-# MINIMAL (==) | (/=) #-}
(==) :: a -> a -> Bool
x == y = not (x /= y)
(/=) :: a -> a -> Bool
x /= y = not (x == y)
This says that a minimal implementation must provide at least one of (==)
and
(/=)
. It is okay for an instance of Eq
to provide implementations for both
(==)
and (/=)
, but this isn't required. We won't go any further into these
annotations, as we can write perfectly correct code without them. When defining
type classes to be used by others, however, it is important to add these
annotations because the user of your class should not have to check the default
implementations to figure out which subset of them need to be overriden to break
infinite recursive dependencies between them.
If you exit and restart GHCi to get rid of our problematic definition of Eq
,
then you're back to using the definition provided as part of the standard
library, which is the one that includes the MINIMAL
pragma. With this
definition of Eq
, we can no longer make BannerNumber
an instance of Eq
without providing any implementation of (==)
or (/=)
:
>>> newtype BannerNumber = B Int
>>> instance Eq BannerNumber
<interactive>:2:10: warning: [-Wmissing-methods]
• No explicit implementation for
either ‘==’ or ‘/=’
• In the instance declaration for ‘Eq BannerNumber’
The compiler gives exactly the expected error message: We are required to
provide a BannerNumber
-specific implementation of at least one of (==)
or
(/=)
. We didn't provide either.1
If we provide an implementation of (==)
, then both (==)
and (/=)
work, the
latter via the default implementation in the Eq
class:
>>> :{
| instance Eq BannerNumber where
| B x == B y = x == y
| :}
>>> B 1 == B 2
False
>>> B 1 /= B 2
True
-
Actually, the compiler only gave us a warning. This means that if we really wanted to, GHCi would still let us try to compare banner numbers at this point, with the same disastrous effect as before. In my opinion, failing to provide required implementations of default methods in type class instances should be an error because no good will come of ignoring this problem in our code. ↩