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 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 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 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 are called by writing their name sandwiched between their parameters. Like the
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.
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.
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.
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:
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
/=, we can learn more:
(==) :: Eq a => a -> a -> Bool (/=) :: Eq a => a -> a -> Bool
=> 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
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.
== has the
Eq a constraint and
== inside, our type signature automatically inherits that constraint:
func :: Eq a => a -> a -> a
Remember the Booleans lesson? We used
/= to compare values of type
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!
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
> can't possibly fit them all.
They may look similar, but
Ord 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,
We already know how to use inequality operators, so let's dive into the other functions.
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
min 12 19 -- 12
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
max 12 19 -- 19
And finally, the compare function.
Check out the
compare type signature:
compare :: Ord a => a -> a -> Ordering
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.
This is a new type for us. In the same way that
Bool has only two values (
Ordering type has only three values:
LT (lesser than),
EQ (equal), and
GT (greater than).
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
Ordtype class, why do I need the
Because you have to learn how to doodle before you can learn how to draw.
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
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
Numbers are a similar case.
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 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
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
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
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.
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
Integral is a more exclusive club than
Num. Of all the types we studied so far, only
Integer belong to it.
This type class defines many behaviors, one of the most well-known
Integral functions is
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.
3 `div` 5 -- 0 div 5 2 -- 2
Now, this doesn't mean that
Double only hang out in the
Num club. In fact, they have their own club!
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?
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.
Take this function that skips the number 3:
skip3 x = if x == 3 then x+1 else x
x can be of any type that is an instance of
Eq (because of
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
p type variable has to be a type that's an instance of both
Num. We could add more constraints if needed.
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
A binary code represents text, computer instructions, or any other data using a two-symbol system. The two-symbol system used is often
1 from the binary number system.
isXBigger x y = if x > y then 1 else 0
In this case,
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
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
mistery1 :: (Ord a, Fractional p) => a -> a -> p -> p
Finally, our last example is a modification of
mistery1 where we add
x before comparing it to
mistery2 x y z = if x+1 > y then z/2 else z
Same as before. But now
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. 😬
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
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.
func3 are equally restrictive because each can accept only two native numeric types.
fromIntegral :: (Integral a, Num b) => a -> b
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!🕵️
- 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!! 🙌 😄