Lesson 14. Using type classes

After reading lesson 14, you’ll be able to

In lesson 13, you got your first look at type classes, which are Haskell’s way of grouping types by common behaviors they share. In this lesson, you’ll take a deeper look at how to implement existing type classes. This will allow you to write new types that take advantage of a wide range of existing functions.

Consider this

You have a data type consisting of data constructors for New England states:

data NewEngland = ME | VT | NH | MA | RI | CT

You want to be able to display them by their full name by using Show. You can easily display their abbreviations by deriving show, but there’s no obvious way to create your own version of show. How can you make your NewEngland type display the full state name by using show?

14.1. A type in need of classes

You’ll start by modeling a six-sided die. A good default implementation is a type similar to Bool, only with six values instead of two. You’ll name your data constructors S1 through S6 to represent each of the six sides.

Listing 14.1. Defining the SixSidedDie data type
data SixSidedDie = S1 | S2 | S3 | S4 | S5 | S6

Next you want to implement some useful type classes. Perhaps the most important type class to implement is Show, because you’ll nearly always want to have an easy way to display instances of your type, especially in GHCi. In lesson 13, we mentioned that you could add the deriving keyword to automatically create instances of a class. You could define SixSidedDie this way and call it a day.

Listing 14.2. The SixSidedDie type deriving Show
data SixSidedDie = S1 | S2 | S3 | S4 | S5 | S6 deriving (Show)

If you were to use this type in GHCi, you’d get a simple text version of your data constructors back when you type them:

GHCi> S1
S1
GHCi> S2
S2
GHCi> S3
S3
GHCi> S4
S4

This is a bit boring because you’re just printing out your data constructors, which are more meaningful from an implementation standpoint than they are readable. Instead, let’s print out the English word for each number.

14.2. Implementing Show

To do this, you have to implement your first type class, Show. There’s only one function (or in the case of type classes, we call these methods) that you have to implement, show. Here’s how to implement your type class.

Listing 14.3. Creating an instance of Show for SixSidedDie
instance Show SixSidedDie where
   show S1 = "one"
   show S2 = "two"
   show S3 = "three"
   show S4 = "four"
   show S5 = "five"
   show S6 = "six"

And that’s it! Now you can return to GHCi and much more interesting output than you would with deriving:

GHCi> S1
one
GHCi> S2
two
GHCi> S6
six
Quick check 14.1

Q1:

Rewrite this definition of show to print the numerals 1–6 instead.

QC 14.1 answer

1:

data SixSidedDie = S1 | S2 | S3 | S4 | S5 | S6

instance Show SixSidedDie where
show S1 = "I"
show S2 = "II"
show S3 = "III"
show S4 = "IV"
show S5 = "V"
show S6 = "VI"

 

14.3. Type classes and polymorphism

One question that might come up is, why do you have to define show this way? Why do you need to declare an instance of a type class? Surprisingly, if you remove your early instance declaration, the following code will compile just fine.

Listing 14.4. Incorrect attempt to implement show for SixSidedDie
show :: SixSidedDie -> String
show S1 = "one"
show S2 = "two"
show S3 = "three"
show S4 = "four"
show S5 = "five"
show S6 = "six"

But if you load this code into GHCi, you get two problems. First, GHCi no longer can print your data constructors by default. Second, even if you manually use show, you get an error:

"Ambiguous occurrence 'show'"

You haven’t learned about Haskell’s module system yet, but the issue Haskell has is that the definition you just wrote for show is conflicting with another that’s defined by the type class. You can see the real problem when you create a TwoSidedDie type and attempt to write show for it.

Listing 14.5. Demonstrating the need for polymorphism defining show for TwoSidedDie
data TwoSidedDie = One | Two

show :: TwoSidedDie -> String
show One = "one"
show Two = "two"

The error you get now is as follows:

Multiple declarations of 'show'

The problem is that by default you’d like to have more than one behavior for show, depending on the type you’re using. What you’re looking for here is called polymorphism. Polymorphism means that the same function behaves differently depending on the type of data it’s working with. Polymorphism is important in object-oriented programming and equally so in Haskell. The OOP equivalent to show would be a toString method, one that’s common among any classes that can be turned into a string. Type classes are the way you use polymorphism in Haskell, as shown in figure 14.1.

Figure 14.1. Visualizing polymorphism for read

14.4. Default implementation and minimum complete definitions

Now that you can produce fun strings for your SixSidedDie, it’d be useful to determine that two dice are the same. This means that you have to implement the Eq class. This is also useful because Eq is the superclass of Ord. You touched on this relationship briefly in lesson 13 without giving it a name. To say that Eq is a superclass of Ord means that every instance of Ord must also be an instance of Eq. Ultimately, you’d like to compare SixSidedDie data constructors, which means implementing Ord, so first you need to implement Eq. Using the :info command in GHCi, you can bring up the class definition for Eq:

class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool

You have to implement only two methods: the Equals method (==) and the Not Equals method (/=). Given how smart Haskell has been so far, this should seem like more work than makes sense. After all, if you know the definition of (==), the definition of (/=) is not (==). Sure, there may be some exceptions to this, but it seems that in the vast majority of cases, if you know either one, then you can determine the other.

It turns out that Haskell is smart enough to figure this out. Type classes can have default implementations of methods. If you define (==), Haskell can figure out what (/=) means without any help.

Listing 14.6. Implementing an instance of Eq for SixSidedDie
instance Eq SixSidedDie where
   (==) S6 S6 = True
   (==) S5 S5 = True
   (==) S4 S4 = True
   (==) S3 S3 = True
   (==) S2 S2 = True
   (==) S1 S1 = True
   (==) _ _ = False

In GHCi, you’ll see that (/=) works automatically!

GHCi> S6 == S6
True
GHCi> S6 == S5
False
GHCi> S5 == S6
False
GHCi> S5 /= S6
True
GHCi> S6 /= S6
False

This is useful, but how in the world are you supposed to know which methods you need to implement? The :info command is a great source of information right at your fingertips, but it isn’t complete documentation. A source of more thorough information is Hackage, Haskell’s centralized package library. Hackage can be found on the web at https://hackage.haskell.org. If you go to Eq’s page on Hackage (https://hackage.haskell.org/package/base/docs/Data-Eq.html), you get much more info on Eq (probably more than you could ever want!). For our purposes, the most important part is a section called “Minimum complete definition.” For Eq, you find the following:

(==) | (/=)

This is much more helpful! To implement the Eq type class, all you have to define is either (==) or (/=). Just as in data declarations, | means or. If you provide either one of these options, Haskell can work out the rest for you.

Hackage and Hoogle

Although Hackage may be the central repository for Haskell information, you might find it a pain to search for specific types. To solve this, Hackage can be searched via a truly amazing interface called Hoogle. Hoogle can be found at www.haskell.org/hoogle. Hoogle allows you to search by types and type signatures. For example, if you search a -> String, you’ll get results for show along with a variety of other functions. Hoogle alone is enough to make you love Haskell’s type system.

Quick check 14.2

Q1:

Use Hoogle to search for the RealFrac type class. What’s its minimal complete definition?

QC 14.2 answer

1:

Go to http://hackage.haskell.org/package/base/docs/Prelude.html#t:RealFrac. The minimal complete definition is properFraction.

 

14.5. Implementing Ord

One of the most important features of dice is that there’s an order to their sides. Ord defines a handful of useful functions for comparing a type:

class Eq a => Ord a where
  compare :: a -> a -> Ordering
  (<) :: a -> a -> Bool
  (<=) :: a -> a -> Bool
  (>) :: a -> a -> Bool
  (>=) :: a -> a -> Bool
  max :: a -> a -> a
  min :: a -> a -> a

Luckily, on Hackage you can find that only the compare method needs to be implemented. The compare method takes two values of your type and returns Ordering. This is a type you saw briefly when you learned about sort in lesson 4. Ordering is just like Bool, except it has three data constructors. Here’s its definition:

data Ordering = LT | EQ | GT

The following is a partial definition of compare.

Listing 14.7. Partial definition of compare for SixSidedDie
instance Ord SixSidedDie where
   compare S6 S6 = EQ
   compare S6 _ = GT
   compare _ S6 = LT
   compare S5 S5 = EQ
   compare S5 _ = GT
   compare _ S5 = LT

Even with clever uses of pattern matching, filling out this complete definition would be a lot of work. Imagine how large this definition would be for a 60-sided die!

Quick check 14.3

Q1:

Write out the patterns for the case of S4.

QC 14.3 answer

1:

compare S4 S4 = EQ

compare _ S4 = LT

Note: Because of pattern matching, the case of compare S5 S4 and compare S6 S4 will already be matched.

compare S4 _ = GT

 

14.6. To derive or not to derive?

So far, every class you’ve seen has been derivable, meaning that you can use the deriving keyword to automatically implement these for your new type definition. It’s common for programming languages to offer default implementations for things such as an .equals method (which is often too minimal to be useful). The question is, how much should you rely on Haskell to derive your type classes versus doing it yourself?

Let’s look at Ord. In this case, it’s wiser to use deriving (Ord), which works much better in cases of simple types. The default behavior when deriving Ord is to use the order that the data constructors are defined. For example, consider the following listing.

Listing 14.8. How deriving Ord is determined
data Test1 = AA | ZZ deriving (Eq, Ord)
data Test2 = ZZZ | AAA deriving (Eq, Ord)

In GHCi, you can see the following:

GHCi> AA < ZZ
True
GHCi> AA > ZZ
False
GHCi> AAA > ZZZ
True
GHCi> AAA < ZZZ
False
Quick check 14.4

Q1:

Rewrite SixSidedDie to derive both Eq and Ord.

QC 14.4 answer

1:

data SixSidedDie = S1 | S2 | S3 | S4 | S5 | S6 deriving (Show,Eq,Ord)

 

With Ord, using the deriving keyword saves you from writing a lot of unnecessary and potentially buggy code.

An even stronger case for using deriving when you can is Enum. The Enum type allows you to represent your dice sides as an enumerated list of constants. This is essentially what we think of when we think of a die, so it’ll be useful. Here’s the definition:

class Enum a where
  succ :: a -> a
  pred :: a -> a
  toEnum :: Int -> a
  fromEnum :: a -> Int
  enumFrom :: a -> [a]
  enumFromThen :: a -> a -> [a]
  enumFromTo :: a -> a -> [a]
  enumFromThenTo :: a -> a -> a -> [a]

Once again, you’re saved by having to implement only two methods: toEnum and fromEnum. These methods translate your Enum values to and from an Int. Here’s the implementation.

Listing 14.9. Implementing Enum for SixSidedDie (errors with implementation)
instance Enum SixSidedDie where
   toEnum 0 = S1
   toEnum 1 = S2
   toEnum 2 = S3
   toEnum 3 = S4
   toEnum 4 = S5
   toEnum 5 = S6
   toEnum _ = error "No such value"

   fromEnum S1 = 0
   fromEnum S2 = 1
   fromEnum S3 = 2
   fromEnum S4 = 3
   fromEnum S5 = 4
   fromEnum S6 = 5

Now you can see some of the practical benefits of Enum. For starters, you can now generate lists of your SixSidedDie just as you can other values such as Int and Char:

GHCi> [S1 .. S6]
[one,two,three,four,five,six]
GHCi> [S2,S4 .. S6]
[two,four,six]
GHCi> [S4 .. S6]
[four,five,six]

This is great so far, but what happens when you create a list with no end?

GHCi> [S1 .. ]
[one,two,three,four,five,six,*** Exception: No such value

Yikes! You get an error because you didn’t handle the case of having a missing value.

But if you had just derived your type class, this wouldn’t be a problem:

data SixSidedDie = S1 | S2 | S3 | S4 | S5 | S6 deriving (Enum)
GHCi> [S1 .. ]
[one,two,three,four,five,six]

Haskell is pretty magical when it comes to deriving type classes. In general, if you don’t have a good reason to implement your own, deriving is not only easier, but also often better.

14.7. Type classes for more-complex types

In lesson 4, we demonstrated that you can use first-class functions to properly order something like a tuple of names.

Listing 14.10. Using a type synonym for Name
type Name = (String,String)

names :: [Name]
names = [ ("Emil","Cioran")
        , ("Eugene","Thacker")
        , ("Friedrich","Nietzsche")]

As you may remember, you have a problem when these are sorted:

GHCi> import Data.List
GHCi> sort names
[("Emil","Cioran"),("Eugene","Thacker"),("Friedrich","Nietzsche")]

The good thing is that clearly your tuples automatically derive Ord, because they’re sorted well. Unfortunately, they aren’t sorted the way you’d like them to be, by last name and then first name. In lesson 4, you used a first-class function and passed it to sortBy, but that’s annoying to do more than once. Clearly, you can implement your own custom Ord for Name.

Listing 14.11. Attempt to implement Ord for a type synonym
instance Ord Name where
   compare (f1,l1) (f2,l2) = compare (l1,f1) (l2,f2)

But when you try to load this code, you get an error! This is because to Haskell, Name is identical to (String, String), and, as you’ve seen, Haskell already knows how to sort these. To solve these issues, you need create a new data type. You can do this by using the data as before.

Listing 14.12. Defining a new type Name using data
data Name = Name (String, String) deriving (Show, Eq)

Here the need for data constructors becomes clear. For Haskell, they’re a way to note, “This tuple is special from the others.” Now that you have this, you can implement your custom Ord.

Listing 14.13. Correct implementation of Ord for Name type
instance Ord Name where
   compare (Name (f1,l1)) (Name (f2,l2)) = compare (l1,f1) (l2,f2)

Notice that you’re able to exploit the fact that Haskell derives Ord on the (String, String) tuple to make implementing your custom compare much easier:

names :: [Name]
names = [Name ("Emil","Cioran")
         , Name ("Eugene","Thacker")
         , Name ("Friedrich","Nietzsche")]

Now your names are sorted as expected:

GHCi> import Data.List
GHCi> sort names
[Name ("Emil","Cioran"),Name ("Friedrich","Nietzsche"),
Name ("Eugene","Thacker")]
Creating types with newtype

When looking at our type definition for Name, you find an interesting case in which you’d like to use a type synonym, but need to define a data type in order to make your type an instance of a type class. Haskell has a preferred method of doing this: using the newtype keyword. Here’s an example of the definition of Name using newtype:

newtype Name = Name (String, String) deriving (Show, Eq)

In cases like this, newtype is often more efficient than using data. Any type that you can define with newtype, you can also define using data. But the opposite isn’t true. Types defined with newtype can have only one type constructor and one type (in the case of Name, it’s Tuple). In most cases, when you need a type constructor to make a type synonym more powerful, newtype is going to be the preferred method.

For simplicity, we’ll stick to creating types with data throughout this book.

14.8. Type class roadmap

Figure 14.2 shows the type classes that are defined in Haskell’s standard library. Arrows from one class to another indicate a superclass relationship. This unit has covered most of the basic type classes. In unit 3, you’ll start exploring the more abstract type classes Semigroup and Monoid, and you’ll start to see how different type classes can be from interfaces. In unit 5, you’ll look at a family of type classes—Functor, Applicative, and Monad—that provide a way to model the context of a computation. Although this last group is particularly challenging to learn, it also allows for some of Haskell’s most powerful abstractions.

Figure 14.2. Type class road map

Summary

In this lesson, our objective was to dive deeper into Haskell’s type classes. You learned how to read type class definitions as well as how to make types an instance of a type class beyond simply using the deriving keyword. You also learned when it’s best to use deriving and when you should write your own instances of a type class. Let’s see if you got this.

Q14.1

Note that Enum doesn’t require either Ord or Eq, even though it maps types to Int values (which implement both Ord and Eq). Ignoring the fact that you can easily use deriving for Eq and Ord, use the derived implementation of Enum to make manually defining Eq and Ord much easier.

Q14.2

Define a five-sided die (FiveSidedDie type). Then define a type class named Die and at least one method that would be useful to have for a die. Also include superclasses you think make sense for a die. Finally, make your FiveSidedDie an instance of Die.