Many Types, One Behavior

In the last two articles, we learned all the basic ways to make our own data types. Now we need to ask a question: what happens when we have multiple types with similar behavior? Polymorphic code can work with many different types. This flexibility makes it easier to work with and better.

Equality

For a concrete example, let’s consider the simple issue of comparing two objects for equality. Let’s review our person data type:

data Person = Person
  { firstName :: String
  , lastName :: String
  , age :: Int
  , hometown :: String
  , homestate :: String }

We’ve seen before how we can compare integers and strings using the equality function (==). What happens when we try to use this with two Person objects?

>> 1 == 1
True
>> “Hi” == “Bye”
False
>> let p1 = Person “John” “Doe” 23 “Kansas City” “Missouri”
>> let p2 = Person “Jane” “Doe” 22 “Columbus” “Ohio”
>> p1 == p2
    No instance for (Eq Person) arising from a use of ‘==’
    In the expression: p1 == p2
    In an equation for ‘it’: it = p1 == p2

This gives us an error. In frustration, we could define our own function for comparing people, using the equality of each field:

personsEqual :: Person -> Person -> Bool
personsEqual p1 p2 = firstName p1 == firstName p2 &&
  lastName p1 == lastName p2 && 
  age p1 == age p2 &&
  hometown p1 == hometown p2 &&
  homestate p1 == homestate p2

The Eq Typeclass

But this is tedious code. It would be a major pain to write out functions like this for every single type we create. How can we make it so we can compare people with (==)? Let’s dig a little deeper into the (==) function by looking at its type:

(==) :: Eq a => a -> a -> Bool

Notice this doesn’t specify a particular type. It uses a generic type a! This explains how it can compare both ints and strings. But why not the Person class? Let’s observe the caveat on this type signature. What does Eq a mean? We saw something similar in the error when we tried to compare our Person objects. Eq is a typeclass. A typeclass is a Haskell construct which ensures certain functions exist describing the behavior of an otherwise generic type. It is similar to an Interface in Java. The Eq typeclass has two functions, (==) and (/=). Here is its definition:

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

These two functions are equality and inequality. We can define them for any class. In order for a data type to belong to the Eq typeclass, it must implement these functions. We make our data type a member of the typeclass by defining an instance for it:

instance Eq Person where
  (==) p1 p2 = firstName p1 == firstName p2 &&
  … as above

We don’t need to define (/=). With Eq, the compiler can determine how to implement (/=) based on our definition of (==), just by negating it with not. With this instance available, we can simply compare Person objects:

>> p1 == p2
False

Deriving Typeclasses

But this isn’t completely satisfying. Because we’re still writing out this tedious definition of the (==) function, when really it should be obvious when two Person objects are the same. They’re the same if all their fields are the same! Haskell is smart enough to figure this out if we tell it, using derived classes:

data Person = Person { …as above... }
  deriving (Eq)

Then the compiler will derive an implementation of (==) and (/=) for the Person object doing exactly what we want them to do. This is pretty awesome!

The Show Typeclass

Let’s observe some other common typeclasses. The Show typeclass is also simple. It has a single method, show, which takes the type and converts it to a String, similar to a toString() method in Java. We use this whenever we want to print something to the console. The compiler can usually derive a Show instance, just like an Eq instance. The only condition is for all the fields of an object also belong to the Show typeclass. But we can also customize it. For instance, we can use this implementation:

instance Show Person where
  show p1 = (firstName p1) ++ “ “ 
    ++ (lastName p1) ++ “, age: “ 
    ++ (show (age p1)) ++ “, born in: “ 
    ++ (hometown p1) ++ “, “ 
    ++ (homestate p1)

And we can compare the printed string for a Person in the two cases.

(Using derived show)
>> let p = Person "John" "Doe" 23 "El Paso" "Texas"
>> p
Person {firstName = "John", lastName = "Doe", age = 23, hometown = "El Paso", homestate = "Texas"}
(Using our show)
>> p
John Doe, age: 23, born in: El Paso, Texas

The Ord Typeclass

Another common typeclass is Ord. We need this typeclass whenever we want to be able to sort or compare objects. It is necessary for certain data structures, like maps. We can derive it, but more often we’ll want special control over how we compare our. For instance, the derived instance of Ord will sort our people first by first name, then by last name, then by age, etc. However, suppose we want a different order. Let’s sort them by age instead, and then by last name, and then by first name. We would define the Ord instance like so:

instance Ord Person where
  compare p1 p2 = if ageComparison == EQ
    then if lastNameComparison == EQ
      then firstName p1 `compare` firstName p2
      else lastNameComparison
    else ageComparison
    where
      ageComparison = (age p1) `compare` (age p2)
      lastNameComparison = (lastName p1) `compare` (lastName p2)

We complete this instance with the single function, compare. This function compares two objects, and determines an ordering. The ordering can be one of three values: LT (less than, meaning the first parameter comes “first”), EQ, or GT (greater than). We can see the difference in the sort order of our people using these two different approaches:

>> let p1 = Person "John" "Doe" 23 "El Paso" "Texas"
>> let p2 = Person "Jane" "Doe" 24 "Houston" "Texas"
>> let p3 = Person "Patrick" "Green" 22 "Boston" "Massachusetts"
(Using derived Ord)
>> sort [p1, p2, p3]
[Jane Doe, age: 24, born in: Houston, Texas,John Doe, age: 23, born in: El Paso, Texas,Patrick Green, age: 22, born in: Boston, Massachusetts]
(Using our Ord)
>> sort [p1, p2, p3]
[Patrick Green, age: 22, born in: Boston, Massachusetts,John Doe, age: 23, born in: El Paso, Texas,Jane Doe, age: 24, born in: Houston, Texas]

Defining a Typeclass

We can even create our own typeclasses. Suppose we have another data type called Student:

data Student = Student
  { studentFirstName :: String
  , studentMiddleName :: String
  , studentLastName :: String
  , studentGradeLevel :: Int }

Notice both the Person class and the Student class have name fields. We could combine these to create a “total” name. We could make a typeclass for this behavior and call it HasName. Then we could provide instances for both types:

class HasName a
  totalName :: a -> String

instance HasName Person where
  totalName p = firstName p ++ “ “ ++ lastName p

instance HasName Student where
  totalName s = studentFirstName s ++ “ “ ++ studentMiddleName s ++ studentLastName s

And we’re done! That’s all it takes to make your own typeclasses!

Summary

  1. Typeclasses allow us to establish common behavior between different types.
  2. Many typeclasses can be derived automatically.
  3. They allow us to use many helpful library methods, like sort.
  4. We can define our own typeclasses.

Now you can use built-in typeclasses and make your own. This opens up a big world of Haskell possibilities, so it’s time to start making your own project! Check out this checklist to make sure you have all the tools to get going!

Previous
Previous

Immutability is Awesome

Next
Next

Making Your Types Readable!