In this session we further consider the relationship between types and functions.
We’ll start by considering the following expression, which reads similarly to English.You cannot type this expression directly into GHCi because it has a variable x
that we have not bound anywhere – but we will get to that.
if (x < 10) then (negate x) else (x + 10)
If x is less than ten, then we will negate x; otherwise, we will add ten.
Each part of this has a type.
Types of subexpressions
First let’s consider just the x < 10
part.
10
is a number, and we’re comparing x
to 10
. This should lead us to conclude that x
also has to be a number; numbers have to be compared to other numbers.
So, if x
is some number, then this expression x < 10
will return a Bool
value. The comparison returns True
or False
; is x less than ten, or is it not?
If x < 10
is True
, then we will take the then
branch and negate x. What should the type of negate x
be?
The negate
function changes the sign of a number.
- If
x
is one (1
), thennegate x
is negative one (-1
). - If
x
is negative one (-1
), thennegate x
is one (1
).
The output from negating a number is the same type of number as the input. The negate
function takes a number, and returns another number of the same type.
Similarly, x + 10
has a type.
Again, we have an operation involving the number 10
, so there are numbers involved. x
must be a number to be added to ten. But unlike x < 10
which produced a yes/no, x + 10
outputs a number – specifically, the same type of number that x
is.
What type of number?
We previously talked about how there are different types of numbers. So what type of number are we dealing with in this expression?
You might be thinking – well, 10
looks like a really solid whole number – it’s probably an integer. But don’t be misled by the way the number is written. There are plenty of other ways to write the number ten, some of which certainly appear fractional:
λ> 10 == 10.0
True
λ> 10 == 10/1
True
Even though we’ve written ten as 10
in the if-then-else expression and not 10.0
, we could still allow x
to be, for example, 5.5
– and that would work fine with this expression.
So, what is the type of the entire expression?
Whatever type of number x
is, that’s the type of the whole expression. But we still haven’t settled on a more specific answer.
Constrained polymorphism
We don’t know what type that is, and we don’t need to know. What we have here is called a polymorphic expression. That means that the function’s input can take on different types, and its output can take on different types of different types (although in this case, the input and output have to be the same as each other).
We talked about how there’s a typeclass called Num
. Since x
has to be a number, but we don’t want to pin down specifically what type of number, we can write our type declaration like this:Here we have taken the expression above and put it in the context of a named function. This allows us to bind the variable x
as a parameter of the function, and gives us the opportunity to write a type declaration for the function.
We have a variable named a
in our type declaration, and this variable is constrained by the Num
typeclass. In this type we are asserting:
- That
a
is a type of number (by the constraintNum a =>
) - That the input and output have to be the same type of number (because the variable
a
appears twice ina -> a
; we did not, for example, writea -> b
, which would allow the two typesa
andb
to differ).
What we’ve written so far isn’t quite right yet.
The Ord class
The negate
and +
functions come from the Num
typeclass, and so from that we inferred that a
needs a Num
constraint. But the <
function comes from a different class, called Ord
.
To get information about types and typeclasses, you can use the :info
command in GHCi. Let’s try that out now.
λ> :info 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
...
This prints a lot of information – more than we need – so you’ll probably need to scroll back up in your terminal to read it from the beginning, and here we’ve truncated the output for brevity’s sake.
The first line class Eq a => Ord a where
and the indented lines immediately below it comprise the typeclass definition. We can see here all of the functions in the Ord
class: compare
, <
, <=
, >
, >=
, max
, and min
.
Recall that many types are orderable, such as characters ('a'
comes before 'b'
), which aren’t numbers. So Ord
is a different typeclass than Num
, because some things are orderable that are not numbers.
So that means we need to add a second constraint on the type variable a
. Whatever type of number x
turns out to be, it has to be both:
- A number, so that it can be negated, compared with ten, and added to ten;
- Something that is orderable, so that it can be compared.
Try it out
Make sure the code is loaded into your REPL session, and try using the function.
λ> :load main.hs
λ> idk 4
-4
Four is less than ten, so 4 is negated to produce -4.
When we apply the function to a bigger number, the else
branch applies:
λ> idk 120
130
Ten is added to 120, and we get 130.
We have this nice polymorphic function that we can use with things other than integers, so let’s make sure we try that out as well.The answer we see here is slightly off due to rounding error in this number’s binary representation. This is one of the reasons there are many types of numbers: in some applications, small roundoffs are acceptable; in others, they are not.
λ> idk 12.13
22.130000000000003
To enter a number fractionally, be sure to write the fraction in parentheses to ensure that the division happens first.
λ> idk (12/5)
-2.4
12/5
is 2.4
, so the result we got back was its negation.
So, to recap this lesson: We have seen how an expression is composed of subexpressions, and every expression has a type. x
and 10
are numbers, x < 10
is a Boolean true/false, negate x
has the same type as x
, and x + 10
does as well. Finally, the if-then-else expression as a whole also has a type – the same type as x
.
Exercise
Next we want you to think about this function:
= if (elem 'j' x) then x else y preferJ x y
We saw this elem
function briefly before.
Try to write a type declaration for the preferJ
function. First, think through what the types of all the subexpressions must be.
- What is the type of
elem 'j' x
? - What are the types of
x
andy
? - Then, finally, what is the type of
preferJ
?
The type declaration you write for preferJ
will look a little different from idk
:
- It may be concrete, not polymorphic (in other words, it does not need any constraints or the
=>
arrow). - This function has two parameters, not one, so it will need two
->
arrows.