Haskell Course - Lesson 8 - Intro to Type classes
Many beginners struggle with type classes. That's because it's a more abstract concept than the ones we have learned so far. But don't worry. We'll see many examples and repeat our reasoning in different circumstances to hone in on the intuitive understanding of the concepts. Hope you like it! 😄
First of all, let's set things straight. You said that functions are used by writing first their name and then their parameters separated by spaces. So, how come the
— The guy that sits in the front row in every class.==
and others like this have one parameter before their name and one after?
Sorry, I overlooked that in the last lesson 😅. Sure, let's see prefix and infix functions first!
Prefix and Infix functions
Prefix and infix are the two ways of calling (using) functions.
Because we don't like to waste time over here, we'll learn these concepts while using a new native function: elem
. The elem
function takes a value and a list of values of the same type and returns True
if the value is inside the list and False
if it isn't.
Prefix functions
Prefix functions are called writing, first their name and then their parameters separated by spaces. For example:
elem 3 [1,2,3] -- True
elem 3 [1,2] -- False
If a function's name has letters and numbers, it's considered a prefix function by default.
Infix functions
Infix functions are called by writing their name sandwiched between their parameters. Like the +
function:
2 + 3 -- 5
If a function's name has only special characters ( +
, /
, =
, and such), it's considered an infix function by default.
We use them differently, but it's not that infix functions are special. In fact, we can easily use a prefix function as an infix function using the infix notation.
Infix notation
To use a prefix function like an infix function, we just have to surround its name with backticks:
3 `elem` [1,2,3] -- True
3 `elem` [1,2] -- False
You could write prefix functions with more than two parameters as infix functions. But I advise against it since you have to add extra parenthesis, and it doesn't read nicely at all! 😵💫
There's also a way to do the inverse (infix to prefix) using the prefix notation.
Prefix notation
To use an infix function like a prefix function, we just have to surround its name with parenthesis:
(+) 2 3 -- 5
(==) 'a' 'b' -- False
It looks less intuitive in these cases. But it's useful in certain circumstances that we may encounter in this course.
OK, with that out of the way, we can see what the heck type classes are.
Type Classes
Object-oriented programming
Object-oriented programming (OOP) is a computer programming model that organizes software design around data—or objects—rather than functions and logic.
In OOP, computer programs are designed by making them out of objects that interact with one another.
Type classes are interfaces that define some behavior. They are like clubs that types can belong to if they have what it takes! 😈
If you meet a guy that belongs to the advanced-drawing club, you know that he can draw. Why? Because it's one of the requirements to enter the club!
Type classes are the same. They have particular behaviors (functions). If a type implements and supports the behaviors of a type class, then the type is an Instance of that type class (a member of that club).
Let's explore a few examples:
The Eq
type class
The Eq
type class is all about equality. The types that are instances of the Eq
type class can say if two values of its type are equal or different by using the ==
(equal) and /=
(not equal) functions.
Looking at the type signatures of ==
and /=
, we can learn more:
(==) :: Eq a => a -> a -> Bool
(/=) :: Eq a => a -> a -> Bool
The =>
symbol is the class constraint
symbol. As you guessed, it indicates that a polymorphic type is constrained to be an instance of a type class. (In this case, the type a
has to be an instance of the type class Eq
.) So, we're constraining (limiting) the types that you can pass to these two functions, from all the types to only those that are instances of the Eq
type class.
It's like a bouncer that lets you in if you're a type that belongs to the Eq
club.
How is this useful to you? Because you can use polymorphic types and still protect yourself from passing the wrong type. 😎 For example, imagine you create this function:
func x y = if x == y then x else y
You don't do math, manipulate strings, or do anything type-dependent. So you can use type variables. But you do check if the values are equal. So you want to make sure that this function only accepts values that can be checked for equality. That's what the Eq
type class is here for. To block you from using types with values that can't be compared.
Because ==
has the Eq a
constraint and func
uses ==
inside, our type signature automatically inherits that constraint:
func :: Eq a => a -> a -> a
Remember the Booleans lesson? We used ==
and /=
to compare values of type Char
, Int
, String
, etc. That's because all those types are instances of the Eq
type class. And Haskell already knows how to check if the values of all those types are equal or not.
So far, all the types that we encounter are instances of this class type (except for functions).
But, you can't do much with types that only belong to the Eq
type class. You can only tell if they are the same exact value or not. Luckily, Eq
is not the only club in town!
The Ord
type class
The Ord
type class is all about order. The types that are instances of the Ord
type class can order their values and say which value is the biggest.
For example, a type that is an instance of the Ord
type class can use the >
(greater than) function:
(>) :: Ord a => a -> a -> Bool
We've already used this function in previous lessons. It takes two values of the same type and returns a boolean:
4 > 9 -- False
How are the values ordered? It depends on the type. With numbers, it follows the mathematical order (e.g., `4` comes before `5` and after `3`). With characters, it follows the Unicode order (like we discussed here). And other types have other rankings.
As you can see, each type has its own way of implementing the behaviors (functions) of the type class. And the type class doesn't care, as long as they have them. It makes sense because each type has its own quirks, and a single definition of ==
or >
can't possibly fit them all.
They may look similar, butOrd
is on an entirely different level than Eq
. Because for >
to be effective, the type has to have some sort of ranking. A way to tell if something is greater than another thing. And this opens the doors to many other functions that derive from this capacity. i.e., all the operators that do inequality testing, compare
, max
, and min
functions.
We already know how to use inequality operators, so let's dive into the other functions.
The min
and max
functions
The min
function takes two values of a type that is an instance of Ord
and returns the minimum of the two:
min :: Ord a => a -> a -> a
Example:
min 12 19 -- 12
The max
function takes two values of a type that is an instance of Ord
and returns the maximum of the two:
max :: Ord a => a -> a -> a
Example:
max 12 19 -- 19
And finally, the compare function.
The compare
function
Check out the compare
type signature:
compare :: Ord a => a -> a -> Ordering
The compare
function takes two values of a type that is an instance of Ord
and returns a value of type Ordering
, indicating the order of the values.
Type what?
This is a new type for us. In the same way that Bool
has only two values (True
and False
), the Ordering
type has only three values: LT
(lesser than), EQ
(equal), and GT
(greater than).
Example:
compare 4 9 -- LT (4 is lesser than 9)
'f' `compare` 'e' -- GT ('f' is greater than 'e')
True `compare` True -- EQ ( True is equal to True)
Because there's not much we can do with values of the Ordering
type, compare isn't used as frequently as the other functions.
Again, so far, all the types we learned are instances of this class type (except for functions).
Now, you might say:
If I can check
EQ
with theOrd
type class, why do I need theEq
type class?
Because you have to learn how to doodle before you can learn how to draw.
Doodling before drawing
Sometimes a type has to first be an instance of one type class to be allowed to become an instance of another. Like you have to belong to the doodling club to be allowed to apply to the drawing club.
That's the case with Eq
and Ord
.
To order values of a type, for starters, you have to be able to tell if they are equal or not. This tells us that if we have a type that is an instance of Ord
, it also supports all the Eq
behavior!
Numbers are a similar case.
Numbers
Numeric types are one of the most used types in any programming language. But not all numeric types can do the same thing, so let's learn about their type classes.
We'll start with the most popular club of all: The Num
type class.
The Num
type class
The types that are instances of the Num
type class can behave like numbers. But not like a specific subset of numbers. The Num
type class defines the behavior of numbers in general.
For example, types that are instances of this type class can be (among other things) added, subtracted, or multiplied:
(+) :: Num a => a -> a -> a
(-) :: Num a => a -> a -> a
(*) :: Num a => a -> a -> a
For example:
5 - 1 -- 4
8.9 + 0.1 -- 9.0
'a' - 'b' -- ERROR! Char is not an instance of Num!
Now we're talking! This is a life savior! Imagine I want to create a function that does some math:
add1 x = x + 1
I don't want to choose a type like Int
and only allow Int
values. Float
, Double
, and Integer
types could work perfectly fine! But, if there were no constraints, I could pass any type! What's the result of 'a' + 1
? Or True + 1
? It doesn't make any sense!
Because only types that are instances of the Num
type class can use +
, and because Float
, Double
, Int
, and Integer
are all instances of Num
, we can constraint our function like this:
add1 :: Num a => a -> a
Now, we can be sure that if Haskell doesn't complain when we use our function, we're safe.
Not only that, we don't even have to manually specify the constraint! If you create that add1
function in GHCi and check the type, you'll get the exact same type signature by default! Haskell knows that to use +
, you have to be an instance of the Num
type, so it infers a type signature for add1
that blocks any other type from using it. Thanks, Haskell! We love you, you little nerd! 🤓
This is awesome! But, sometimes, we need something more specific.
The Integral
type class
By now, you know the drill. The Num
type class includes all the numbers, and the Integral
type class only the whole (or integral) numbers. Such as 4
, but not 4.3
.
Integral
is a more exclusive club than Num
. Of all the types we studied so far, only Int
and Integer
belong to it.
This type class defines many behaviors, one of the most well-known Integral
functions is div
.
div :: Integral a => a -> a -> a
It takes two values of a type that is an instance of Integral
and divides them, returning only the whole part of the division.
Examples:
3 `div` 5 -- 0
div 5 2 -- 2
Now, this doesn't mean that Foat
and Double
only hang out in the Num
club. In fact, they have their own club!
The Fractional
type class
The Fractional
type class is all about fractional numbers. The types that are instances of the Fractional
type class can represent and modify fractional values.
By far, the most valuable function that instances of the Fractional
type class can use is /
:
(/) :: Fractional a => a -> a -> a
The all-mighty division. Unlike div
, we can be more precise about our values because we're using fractional numbers. For example:
10 / 5 -- 2.0
5 / 2 -- 2.5
10 / 3 -- 3.3333333333333335
Which reminds me, what about the function we created to transform from Fahrenheit to Celsius? 🤔
fToC x = (x - 32)*5/9
It takes a value and returns a value.
fToC :: a -> a
The value that it takes must be a numeric type (we're doing math with several mathematical functions).
fToC :: Num a => a -> a
But not any numeric type will do! It has to be a type that can be divided using /
.
fToC :: Fractional a => a -> a
At the end of the day, the most restrictive constraint wins.
Until now, we've been restricting if the type is an instance of a particular type class. And we know that there can be more specialized type classes (Fractional
is a more specialized type class than Num
). But what if you need a type that can both draw AND play basketball?
Multiple constraints
Applying constraints is more flexible than it seems. Sometimes you need different constraints for different type variables. Or the same type variable with multiple constraints. All this can be easily expressed in Haskell.
Multiple constraints for the same type variable
Take this function that skips the number 3:
skip3 x = if x == 3 then x+1 else x
The x
can be of any type that is an instance of Eq
(because of ==
) and Num
(because of +
). To specify multiple constraints for the same type variable, we have to surround them with parenthesis and add a comma between them. Like if they were a tuple:
skip3 :: (Eq p, Num p) => p -> p
Now the p
type variable has to be a type that's an instance of both Eq
and Num
. We could add more constraints if needed.
Constraints for multiple type variables
Let's create a function that takes two values and returns 1
if the first value is greater than the second, and 0
otherwise. (In
Binary code
A binary code represents text, computer instructions, or any other data using a two-symbol system. The two-symbol system used is often 0
and 1
from the binary number system.
1
usually means True
and 0
means False
).isXBigger x y = if x > y then 1 else 0
In this case, x
and y
have to be instances of Eq
. And the return value is a number of an unspecified type, so it's an instance of Num
. Putting this together, the type signature will be:
isXBigger :: (Ord a, Num p) => a -> a -> p
Now, what about this function?
mistery1 x y z = if x > y then z/2 else z
We compare x
and y
, so they have to be instances of Ord
type class. And the return value it's divided using /
in one of the if-else paths. So z
has to be an instance of Fractional
.
mistery1 :: (Ord a, Fractional p) => a -> a -> p -> p
Finally, our last example is a modification of mistery1
where we add 1
to x
before comparing it to y
:
mistery2 x y z = if x+1 > y then z/2 else z
Same as before. But now x
and y
also have to be an instance of Num
to be able to use +
.
mistery2 :: (Ord a, Num a, Fractional p) => a -> a -> p -> p
As you can see, we can apply as much constraints as needed. Of course, Haskell will apply them for you (most of the time). And you'll have to correctly interpret and understand them.
There are tons of type classes and behaviors that we didn't cover today, but don't worry about it until you cross paths with them. It's no use to memorize a bunch of behaviors and type classes if you don't actively work with them.
I hope that, by now, you feel comfortable reading—and appreciating the value of—type constraints. To help you with that, I've prepared some questions. 😬
Homework
Type signature practice
Guess the type signature of func
without using GHCi:
func a b = if a > 18 then "You can pass, " ++ b else "Sorry, " ++ b ++ ". Too young!"
func :: (Ord a, Num a) => a -> [Char] -> [Char]
a
has to be an instance of Ord
to use >
and Num
to be compared to a number.
Which function is more type-restrictive?
func1 x y = x*2 + y
func2 x y = x/2 + y
func3 x y = x `div` 2 + y
func1 :: Num a => a -> a -> a
func2 :: Fractional a => a -> a -> a
func3 :: Integral a => a -> a -> a
func1
is the less restrictive of the three, because all native numeric types are accepted. func2
and func3
are equally restrictive because each can accept only two native numeric types.
Last question:
fromIntegral :: (Integral a, Num b) => a -> b
Install VSCode
We'll need a text editor for our next lesson. You can use whichever you want, but if you haven't coded before, I recommend VSCode.
I won't explain step-by-step how to install a program as popular as VSCode. But I'll tell you exactly what we need, and you'll have to figure it out. Like a proper software developer!🕵️
We need:
- VSCode.
- Two VSCode extensions:
- Haskell Syntax Highlighting.
- Haskell (Haskell language support powered by the Haskell Language Server).
If you look around for tutorials, you'll cross paths with other tools like Cabal or Stack. We'll eventually use them, but not right now. You can safely ignore them. Search away!! ⛵️
We've equipped ourselves with enough knowledge to start doing some interesting things. And that's what we're going to do the next lesson! WE'RE GOING TO CODE OUR FIRST PROJECT! 🤩 So, make sure you complete today's homework (especially the last part) to be ready! See you there!! 🙌 😄
PD: If you find this course valuable, please share it so more people can learn! 😄