Learning Haskell: Typeclasses

posted 2 years ago

Typeclasses

A typeclass defines how a set of types are consumed or used in computations.

Typeclasses allow us to generalize over a set of types in order to define and execute a standard set of features for those types.

If you are familiar with concepts in other programming languages, typeclasses are similar to interfaces.

Revisiting Bool

Let's look at the data declaration for Bool and which typeclasses it already has instances of:

Prelude> :info Bool
data Bool = False | True    -- Defined in ‘GHC.Types’
instance Eq Bool -- Defined in ‘GHC.Classes’
instance Ord Bool -- Defined in ‘GHC.Classes’
instance Enum Bool -- Defined in ‘GHC.Enum’
instance Show Bool -- Defined in ‘GHC.Show’
instance Read Bool -- Defined in ‘GHC.Read’
instance Bounded Bool -- Defined in ‘GHC.Enum’

Each of these instances is a typeclass that Bool implements, and the instances are the unique specifications of how Bool makes use of the methods from that typeclass.

  1. instance Bounded Bool - Bounded for types that have an upper and lower bound.
  2. instance Enum Bool - Enum for things that can be enumerated.
  3. instance Eq Bool - Eq for things that can be tested for equality.
  4. instance Ord Bool - Ord for things that can be put into sequential order.
  5. instance Read Bool - Read parses strings into other things.
  6. instance Show Bool - Show renders things into strings.

Typeclasses have a hierarchy.

For example, all Fractional numbers implement the Num typeclass, but not all Num are Fractional.

All members of Ord must be members of Eq, and all members of Enum must be members of Ord.

Eq

In Haskell, equality is implemented using the Eq typeclass.

Eq defined:

-- not all listed, check for yourself :)
instance Eq a => Eq [a] -- Defined in ‘GHC.Classes’
instance Eq Word -- Defined in ‘GHC.Classes’
instance Eq Ordering -- Defined in ‘GHC.Classes’
instance Eq Int -- Defined in ‘GHC.Classes’
instance Eq Float -- Defined in ‘GHC.Classes’
instance Eq Double -- Defined in ‘GHC.Classes’
instance Eq Char -- Defined in ‘GHC.Classes’
instance Eq Bool -- Defined in ‘GHC.Classes’

There are several numeric types, there is Bool, Char, tuples, and much more.

Anytime that we are using data of these types, we are implementing the Eq typeclass and therefore have generic functions we can use to compare their equality.

Examples of Eq in action:

Prelude> 1 == 1
True
Prelude> 1 /= 1
False

Let's look at the information for (==) and (/=):

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

With this information, we know that they can be used for any type a which implements the Eq typeclass.

Writing Typeclass Instances

You can write your own datatypes and typeclasses.

For example, to make a minimal valid Eq instance, you must define either (==) or (/=).

Let's make our own datatype; I will call it Trite.

data Trite
  = Trite

If we try to test equality on this newly created type, GHC will throw a type error:

Prelude> Trite == Trite 

<interactive>:2:1: error:
    • No instance for (Eq Trite) arising from a use of ‘==’
    • In the expression: Trite == Trite
      In an equation for ‘it’: it = Trite == Trite

We get this error because GHC can't find an instance of Eq for our datatype Trite.

Let's write our own!

data Trite
  = Trite'

instance Eq Trite where
   [1]   [2] [3]   [4]
  Trite' == Trite' = True
   [5]  [6]  [7]     [8] 
  1. The keyword instance beings a declaration of a typeclass instance. Typeclass instances allow you to tell Haskell how typeclasses should work for a particular datatype. Without this instance, we would not be about to test the values for equality.
  2. The typeclass that the instance is providing; in this case it is Eq.
  3. The type that the instance is being provided for. We are implementing the Eq typeclass for the Trite datatype.
  4. The keyword where terminates the initial declaration and beginning of the instance. After this, the methods are implemented.
  5. The data constructor Trite'is the first argument to the (==) function. We are defining == using infix notation here.
  6. (==) in infix notation.
  7. The second argument, which is the value Trite'.
  8. The result of Trite' == Trite' which is True.

Now the compiler knows how to test this datatype for equality. I changed the example to use a single quote at the end of the data constructor because they don't have to have the same name. This was done only for readability purposes.

Let's try testing it out:

Prelude> Trite' == Trite'
True

We can make a less trite example. Let's create a Colors datatype and a Creature datatype. The Creature datatype describes a creature and the Colors datatype describes the color of the creature.

data Colors
  = Red
  | Orange
  | Yellow
  | Green
  | Blue
  | Indigo
  | Violet

data Creature
  = Creature Colors

instance Eq Colors where
  (==) Red Red = True
  (==) Orange Orange = True
  (==) Yellow Yellow = True
  (==) Green Green = True
  (==) Blue Blue = True
  (==) Indigo Indigo = True
  (==) Violet Violet = True
  (==) _ _ = False -- exhaustive check

instance Eq Creature where
  (==)
    (Creature color)
    (Creature color') =
      color == color'

First, we wrote Eq for Colors. Next, we wrote Eq for Creature. We test for equality by seeing if all of their constituent values are equal.

Testing it:

Prelude> Creature Orange == Creature Orange
True
Prelude> Creature Red == Creature Blue
False

Writing an instance of a typeclass for something with polymorphic parameters

We sometimes need to require our argument or arguments to provide some typeclass instances for us in order to write an instance for the datatype containing them

data Example a
  = Example a

instance Eq (Example a) where
  (==) (Example e) (Example e') = (==) e e'

We know that e and e' are both of type a, but we don't know anything about a. We can't assume that a has an Eq instance.

Because of this, we use a typeclass constraint in the instance declaration.

data Example a
  = Example a

instance Eq a => Eq (Example a) where
  (==) (Example e) (Example e') = (==) e e'

Now, we know that a has an instance of Eq.

Num

We have already used Num several times, but to reiterate, Num is a typeclass implemented by most numeric types.

Let's do the same thing with Num as we did with Eq and query its information.

class Num a where -- left out some stuff
  (+) :: a -> a -> a
  (-) :: a -> a -> a
  (*) :: a -> a -> a
  negate :: a -> a
  abs :: a -> a
  signum :: a -> a
  fromInteger :: Integer -> a 
    -- Defined in ‘GHC.Num’
instance Num Integer -- Defined in ‘GHC.Num’
instance Num Int -- Defined in ‘GHC.Num’
instance Num Float -- Defined in ‘GHC.Float’
instance Num Double -- Defined in ‘GHC.Float’

We've seen most of this before so no surprises here.

Integral

Integral is a typeclass that has the following definitions:

class (Real a, Enum a) => Integral a where
  quot :: a -> a -> a
  rem :: a -> a -> a
  div :: a -> a -> a
  mod :: a -> a -> a
  quotRem :: a -> a -> (a, a)
  divMod :: a -> a -> (a, a)
  toInteger :: a -> Integer

The typeclass constraint (Real a, Enum b) => means that any type that implements Integral must already have instances for Real and Enum typeclasses.

Fractional

Num is a superclass of Fractional. The Fractional typeclass is defined as follows:

class Num a => Fractional a where
  (/) :: a -> a -> a
  recip :: a -> a
  fromRational :: Rational -> a

Fractional requires an argument a to have an instance of Num.

This is an example of typeclass inheritance. Fractional applies to fewer numbers than Num does, and instances of the Fractional class can use the functions defined in Num, but not all Num can use the functions defined in Fractional. This is because nothing in the Num definition requires an instance of Fractional.

Type-Defaulting Typeclasses

When you have a typeclass-constrained polymorphic value and need to evaluate it, the polymorphism must be resolved to a specific concrete type.

The concrete type must have an instance for all the required typeclass instances.

For example:

1 / 2 -- the result of this defaults to a Double

You must specify which typeclasses you want type variables to have if they are not concrete.

In this case, the typeclass constraint is superfluous because the types are concrete.

We can make types more specific:

let x = 10 :: Integer
let y = 4 :: Double

Types can be made more specific, but not more general or polymorphic.

Ord

We have previously noticed that Ord is the typeclass that covers the types of things that can be put in order.

Let's take a look at a parsed version of the info:

Prelude> :i Ord
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

The functions that come standard in this class have to do with ordering. Some of them give a result of Bool as we've seen in the past.

Remember Ord instances cannot exist without also having an instance of Eq.

This logically makes sense because you can't order things without the ability to check for equality.

Enum

Enum is a typeclass that is similar to Ord but slightly different.

Enum covers types that are enumerable.

Enumerables have predecessors and successors.

You probably have a good idea about how to check typeclass information at this point so I'll leave that to you.

The two main functions that Enum has are succ and pred.

Prelude> succ 2
1
Prelude> pred 2
3

Here are two other functions that are pretty neat: enumFromTo and enumFromThenTo

Prelude> enumFromTo 1 5
[1,2,3,4,5]
Prelude> enumFromThenTo 1 10 100
[1,10,19,28,37,46,55,64,73,82,91,100]

Show

Show is a typeclass that provides for creating readable string representations.

When you ask GHCi to return the result of an expression and print it to the screen, you are indirectly invoking a function called print.

Printable types are those that are instances of class Show.

Creating an instance of Show:

data Shape
  = Circle

instance Show Shape where
  show _ = "Circle"

Read

Read is the opposite of Show.

I would recommend not using Read because it doesn't always work and there are better alternatives in Haskell.

I will be going over better alternatives at a later date.

At the end of the day, you can choose to use it or not, but I wouldn't.

Don't just take my word for it; do your own research.

Instances are Dispatched by Type

Typeclasses are defined by the set of operations and values all instances will provide.

Typeclass instances are unique pairings of the typeclass and a type.

Remember:

  • a typeclass defines a set of functions and/or values
  • types have instances of that typeclass
  • the instances specify the ways that type uses the functions of the typeclass

Sources

An introduction to typeclass metaprogramming - https://lexi-lambda.github.io/blog/2021/03/25/an-introduction-to-typeclass-metaprogramming/

Type Classes in Haskell - http://ropas.snu.ac.kr/lib/dock/HaHaJoWa1996.pdf

Haskell Book - https://haskellbook.com/