Types and functions, continued

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), then negate x is negative one (-1).
  • If x is negative one (-1), then negate 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.

idk :: (Num a) => a -> a
idk x =
    if (x < 10) then (negate x) else (x + 10)

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 constraint Num a =>)
  • That the input and output have to be the same type of number (because the variable a appears twice in a -> a; we did not, for example, write a -> b, which would allow the two types a and b 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:

preferJ x y = if (elem 'j' x) then x else 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 and y?
  • 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.

Join Type Classes for courses and projects to get you started and make you an expert in FP with Haskell.