In this note we will see how we can define our own instances of a type class. As a running example, consider the Card class and its friends, that we used in assignment 3:
data Suit = Clubs | Diamonds | Hearts | Spades
data Value = Ace | Num Int | Jack | Queen | King
data Card = Cd Suit Value
In the assignment, we automatically derived definitions for Eq and Show as well as Ord. We also used simply pairs for the cards. But in order to assign our own implementations of comparisions, we have to create a custom data type, so we use the prefix Cd for cards.
We will now implement “manual” definitions of Eq, Show and Ord, with slight variations.
instance Eq Suit where
Clubs == Clubs = True
Diamonds == Diamonds = True
Hearts == Hearts = True
Spades == Spades = True
_ == _ = False
instance Ord Suit where
compare Clubs Clubs = EQ
compare Clubs _ = LT
compare Diamonds Clubs = GT
compare Diamonds Diamonds = EQ
compare Diamonds _ = LT
compare Hearts Spades = LT
compare Hearts Hearts = EQ
compare Hearts _ = GT
compare Spades Spades = EQ
compare Spades _ = GT
After these definitions, we can ask, for example whether Club < Diamonds and then get the answer True: The default implementation for < using our compare function kicks in.
For Show let’s do something different! Each suit has a Unicode character corresponding to it. They come in “black” and “white” variants, depending on whether their interior is filled or not.
instance Show Suit where
show Clubs = "\x2663"
show Diamonds = "\x2666"
show Hearts = "\x2665"
show Spades = "\x2660"
Now we can do something like this and see a beautiful symbol for a club: putStrLn $ show Clubs.
Let’s further define Enum and Bounded instances. Bounded is easy:
instance Bounded Suit where
minBound = Clubs
maxBound = Spades
We can now ask for minBound :: Suit and we will see the Clubs symbol printed out.
instance Enum Suit where
toEnum 0 = Clubs
toEnum 1 = Diamonds
toEnum 2 = Hearts
toEnum 3 = Spades
fromEnum Clubs = 0
fromEnum Diamonds = 1
fromEnum Hearts = 2
fromEnum Spades = 3
After that definition, we can do [minBound .. maxBound] :: [Suit] and see a list of the four suits.
Next, we will create instances of Eq, Ord, Bounded, Enum and Show for the Value type:
instance Eq Value where
Ace == Ace = True
Jack == Jack = True
Queen == Queen = True
King == King = True
Num x == Num y = x == y
_ == _ = False
instance Ord Value where
compare Ace Ace = EQ
compare Jack Jack = EQ
compare Queen Queen = EQ
compare King King = EQ
compare Ace _ = LT
compare _ Ace = LT
compare _ King = LT
compare King _ = GT
compare _ Queen = LT
compare Queen _ = GT
compare _ Jack = LT
compare Jack _ = GT
compare (Num n) (Num m) = compare n m
instance Bounded Value where
minBound = Ace
maxBound = King
instance Enum Value where
toEnum 1 = Ace
toEnum 11 = Jack
toEnum 12 = Queen
toEnum 13 = King
toEnum n = Num n
fromEnum Ace = 1
fromEnum Jack = 11
fromEnum Queen = 12
fromEnum King = 13
fromEnum (Num n) = n
instance Show Value where
show Ace = "A"
show (Num n) = show n
show Jack = "J"
show Queen = "Q"
show King = "K"
Now we should implement the same functionality for Card, which consists of a suit and a value. The convention we will follow is that “smaller values come first”. So we first compare the values and then compare the suits.
instance Eq Card where
Cd s1 v1 == Cd s2 v2 = s1 == s2 && v1 == v2
instance Ord Card where
Cd s1 v1 `compare` Cd s2 v2 = compare v1 v2 `orElse` compare s1 s2
where EQ `orElse` o = o
o `orElse` _ = o
We “show” a card by showing the value and the suit next to each other:
instance Show Card where
show (Cd s v) = show v ++ show s
We can easily make Card and instance of Bounded too:
instance Bounded Card where
minBound = Cd minBound minBound
maxBound = Cd maxBound maxBound
Now minBound :: Card brings up the Ace of Clubs.
Lastly, Enum. We want to make sure we keep the ordering of the cards, starting with the 13 clubs cards at 1-13, then the diamonds cards 14-26, and so on. In order to do that, we can do some “modulo 13” math on the values of suits and cards. We need to do a bit of work for the toEnum function, because the numbers are “1-13” instead of “0-12”.
instance Enum Card where
fromEnum (Cd s v) = fromEnum v + 13 * fromEnum s
toEnum n = Cd s v where s = toEnum ((n-1) `div` 13)
v = toEnum ((n-1) `mod` 13 + 1)
Now we can put all the cards in one list easily:
[minBound .. maxBound] :: [Card]