In this article, I'm exploring classes in Haskell. Coming from object oriented languages, the concept of Haskell classes was a bit confusing for me. Haskell is a functional programming language, so its class functionality doesn't match classes in object oriented languages such as Java or Python. The closest comparison for Haskell classes in the object oriented world is Java interfaces with default methods1. This article helps clear the confusion of Haskell classes.
My previous Haskell post looked at declaring custom types. Custom types and existing types can become instances of classes, giving them functionality. Type classes are created with a class
declaration and instances are created with an instance
declaration.
As I mentioned earlier, a class is similar to a Java interface. Therefore it provides operator and function declarations without bodies. For example, the following Eq
class matches the one in the Prelude
module that checks for equality.
class Eq a where
(==), (/=) :: a -> a -> Bool
x /= y = not (x == y)
The class Eq
defines two operators, ==
and /=
. It also creates a default definition for the /=
operator (which is simply the opposite of ==
). If an instance of Eq
does not implement the /=
operator, its functionality falls back to the default definition2.
A type is defined as an instance of a class using an instance
declaration. The following code declares Int
as an instance of Eq
.
instance Eq Int where
(==) = eqInt
(/=) = neInt
While I didn't have to implement (/=)
due to the default definition, there is no harm in implementing it anyway. eqInt
and neInt
are internal Haskell functions used to determine integer equality.
Thanks to the Eq
class and corresponding instance declaration, any two Int
types can be used with the ==
and /=
operators.
main :: IO ()
main = do
print $ (1 :: Int) == (1 :: Int) -- True
print $ (2 :: Int) == (1 :: Int) -- False
Just like built-in types, instances can be created with custom types. The following code creates a custom FirTree
type and declares it as an instance of Eq
.
-- Create a type for fir trees of different species. A tree needs to be provided a height in feet and inches
data FirTree = FrasierFir Int Int | BalsamFir Int Int | DouglasFir Int Int
{-|
- Make FirTree an instance of Eq. In order to be equal, the species of tree must be the same
- and the trees must have the same height.
-}
instance Eq FirTree where
(FrasierFir f1 i1) == (FrasierFir f2 i2) = (f1 == f2) && (i1 == i2)
(BalsamFir f1 i1) == (BalsamFir f2 i2) = (f1 == f2) && (i1 == i2)
(DouglasFir f1 i1) == (DouglasFir f2 i2) = (f1 == f2) && (i1 == i2)
_ == _ = False
I tested the ==
and /=
operators with FirTree
types.
main :: IO ()
main = do
let balsam1 = BalsamFir 6 2
let balsam2 = BalsamFir 7 4
let balsam3 = BalsamFir 6 2
print $ balsam1 == balsam2 -- False
print $ balsam1 == balsam3 -- True
let frasier1 = FrasierFir 6 2
let frasier2 = FrasierFir 7 4
let frasier3 = FrasierFir 6 2
print $ frasier1 == frasier2 -- False
print $ frasier1 == frasier3 -- True
print $ frasier1 == balsam1 -- False
Type classes can extend one of many existing classes. Class extensions are a form of class inheritance, and a class that extends multiple classes demonstrates multiple inheritance. I do feel nervous speaking of Haskell classes in object oriented terms simply because there are many differences between the two. These differences become apparent when dealing with multiple inheritance and class extensions.
A simple example of a class extension from the Prelude
module is the Ord
class which extends Eq
.
class (Eq a) => Ord a where
(<), (<=), (>=), (>) :: a -> a -> Bool
max, min :: a -> a -> a
-- Default methods
min x y | x <= y = x
| otherwise = y
max x y | x <= y = y
| otherwise = x
One of the major misconceptions I had about class extensions was a result of my object oriented background. I figured that instances of Ord
would inherit the operators and functions from Eq
, but that is not the case. What a class extension actually means is that for a type to be an instance of Ord
it must also be an instance of Eq
3. This is much different than object oriented class inheritance.
I created a full diamond problem of Haskell class extensions, similar to my object oriented multiple inheritance example.
-- Types of fir trees
data FrasierFir = FrasierFir Int Int
deriving Show
data BalsamFir = BalsamFir Int Int
deriving Show
data DouglasFir = DouglasFir Int Int
deriving Show
-- Grades for different qualities of the fir trees
data Grade = Fair | Good | Excellent
deriving Show
{-|
- A class for a biological tree. Each tree must be able to calculate its height
-}
class Tree a where
-- Class operator
height :: a -> Int
-- Default definition
height _ = 0
{-|
- A class for a Christmas tree which is an extension of the biological tree class.
-}
class (Tree a) => ChristmasTree a where
-- Class operator
holiday_tree :: a -> Bool
-- Default definition
holiday_tree _ = True
{-|
- A class for an evergreen tree which is an extension of the biological tree class.
-}
class (Tree a) => EvergreenTree a where
-- Class operator
leaf_persistence :: a -> Bool
-- Default definition
leaf_persistence _ = True
{-|
- A class for a fir tree, which is an extension of both a Christmas tree and an evergreen tree.
-}
class (ChristmasTree a, EvergreenTree a) => FirTree a where
-- Class operators
fragrance :: a -> Grade
ease_to_decorate :: a -> Grade
needle_retention :: a -> Grade
To make the FrasierFir
, BalsamFir
, and DouglasFir
types instances of FirTree
, they first must be instances of Tree
, ChristmasTree
, and EvergreenTree
.
{-|
- FrasierFir is an instance of FirTree. Since it is an instance of FirTree, it must also be an instance
- of ChristmasTree, EvergreenTree, and Tree
-}
instance FirTree FrasierFir where
fragrance a = Fair
ease_to_decorate a = Good
needle_retention a = Excellent
instance ChristmasTree FrasierFir
instance EvergreenTree FrasierFir
instance Tree FrasierFir where
height (FrasierFir x y) = (x * 12) + y
{-|
- DouglasFir is an instance of FirTree. Since it is an instance of FirTree, it must also be an instance
- of ChristmasTree, EvergreenTree, and Tree
-}
instance FirTree DouglasFir where
fragrance a = Good
ease_to_decorate a = Fair
needle_retention a = Excellent
instance ChristmasTree DouglasFir
instance EvergreenTree DouglasFir
instance Tree DouglasFir where
height (DouglasFir x y) = (x * 12) + y
{-|
- BalsamFir is an instance of FirTree. Since it is an instance of FirTree, it must also be an instance
- of ChristmasTree, EvergreenTree, and Tree
-}
instance FirTree BalsamFir where
fragrance a = Excellent
ease_to_decorate a = Excellent
needle_retention a = Fair
instance ChristmasTree BalsamFir
instance EvergreenTree BalsamFir
instance Tree BalsamFir where
height (BalsamFir x y) = (x * 12) + y
Next I tested the functionality provided by the type classes.
main :: IO ()
main = do
let frasier = FrasierFir 7 2
print $ frasier -- FrasierFir 7 2
print $ fragrance frasier -- Fair
print $ ease_to_decorate frasier -- Good
print $ needle_retention frasier -- Excellent
print $ leaf_persistence frasier -- True
print $ holiday_tree frasier -- True
print $ height frasier -- 86
let balsam = BalsamFir 5 6
print $ balsam -- BalsamFir 5 6
print $ fragrance balsam -- Excellent
print $ ease_to_decorate balsam -- Excellent
print $ needle_retention balsam -- Fair
let douglas = DouglasFir 10 2
print $ douglas -- DouglasFir 10 2
print $ fragrance douglas -- Good
print $ ease_to_decorate douglas -- Fair
print $ needle_retention douglas -- Excellent
As this example demonstrates, Haskell provides multiple inheritance of classes. However, Haskell classes are more like interfaces, and instances of subclasses must explicitly be instances of all parent classes in the inheritance hierarchy.
Type classes in Haskell are much different than the ones found in object oriented languages. I'm excited to learn how to use them in more complex Haskell programs.