Basic types, part 2

Video
  • 11 minutes

Next we want to give you some more types to play with, so we can start to do things that are perhaps more interesting than arithmetic.

Branching, generalized

First we should take another look at the case keyword. Let’s go back to that conditional statement that we wrote in the last section.

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

We have already noticed that this branches on the result of an expression (x < 10) that evaluates to a Bool result. We also saw when we wrote the spell function how a function involving a case ... of can match columns of inputs to outputs.

The if-then-else form is great when what you have is a Bool, but it can’t take you any farther – it’s only useful for Bool. What comes after the if always has to be true or false. So we need to make sure we understand how to generalize this notion of branching and write expressions that can branch over anything.

What we’ll do now, just to get a little more case practice, is rewrite the idk function so that it uses a case expression instead of if-then-else. We’ll call it idk2 just to give it a different name.

The type of the function will remain the same, so we can just copy the type declaration and change the name.

idk2 :: (Ord a, Num a) => a -> a

It’s the syntax of the function description itself that changes. Instead of saying if (x < 10), we’ll say case (x < 10) of.

idk2 :: (Ord a, Num a) => a -> a
idk2 x =
    case (x < 10) of
        (...)

Instead of the then and else words, we’ll give a list of the possible Boolean result values and map them to the desired outputs, in a sort of tabular form like we did with spell.

idk2 :: (Ord a, Num a) => a -> a
idk2 x =
    case (x < 10) of
        True -> _____
        False -> _____

Please take a moment to figure out what should go in each of the two blanks.


What we had previously written after the then keyword goes after the arrow for True, and the else corresponds to False. So it should end up looking like this:There are three expressions here we have parenthesized for clarity; in all three cases, the parentheses are optional. It never hurts to add them if you are unsure.

idk2 :: (Ord a, Num a) => a -> a
idk2 x =
    case (x < 10) of
        True -> (negate x)
        False -> (x + 10)

This is called pattern matching. The patterns here are True and False.These probably don’t look like “patterns”, because they are extremely simple patterns. But once we see more complex examples, it may become more clear why what’s written on the left side of an -> arrow is called a pattern. And the result is given by what’s on the right side of the arrow, based on which pattern matches the result of x < 10 for a given x.

Maybe

Let’s look next at another type that is very useful in Haskell and can be thought of as encoding a kind of two-valued logic. The type is called Maybe and, instead of choosing between false and true, it gives us a choice between nothing and something.

data Maybe a = Nothing | Just a

The biggest difference between Bool and Maybe is that Maybe has a type parameter.

The Maybe part of this, because it has a parameter, is described most precisely not as a type, but as a type constructor.

The a is the type parameter in this definition.Sometimes we use more descriptive names for type variables. But in this case, since this variable represents any type at all, and Maybe does not ascribe any particular meaning to it, there is really nothing to describe, no information that we might want to communicate via the choice of name. In cases like these, we conventionally use single letters a, b, c as parameter names. This is similar to the function parameters we’ve seen: it’s a variable that can stand for many different things. Whereas our idk function needed to be applied to a value of a numeric type, though, Maybe has to be applied to another type. Importantly, since there is no constraint on the a (that is, nothing like Num a =>) this a could be any type. Maybe can take any type and turn it into a new type Maybe a that has the possibility of being Just one value of that type or of being Nothing.

Just like what we saw in a function definition, a variable appears on both the left and right side of the =.

  • The place where the variable appears to the left of the equals sign is called the binding; this assigns the name a to the variable.
  • Wherever a appears on the right is where we are using what we bound on the left.

The a in the Just a has to be a value of the type that we applied Maybe to.

Because Just has a parameter, we can use it like a function. For example, we can apply the Just constructor to the string "Julie". Because we’ve given it a String, the type of the overall expression is Maybe String.

Just "Julie" :: Maybe String

For another example, suppose we apply the Just constructor to the value 3. Just as we saw before, what we end up here is a polymorphic expression, because we haven’t specified exactly what type of number the 3 is. So the type of this expression is written as Maybe a – but it definitely does have to be a number, so there is also a Num constraint on the a.

Just 3 :: Num a => Maybe a

So why does this Maybe type exist – why would we want to take a perfectly good type and add a Nothing to it?

One thing that Maybe is useful for is giving us a way to “fail” functions by returning a result of Nothing which might mean we didn’t get an input that we consider valid. For instance, we might have wanted to use that in our spell function, to return Nothing instead of “I don’t know this number!” as the result for numbers that we don’t know how to write as words. We’ll be using it in this way a few lessons later when we write a small program.

List

We’re also going to need lists for our forthcoming program. So, without dwelling on all the details, we are going to next take a brief look at the list datatype now.Because [] is a built-in type with special syntax in Haskell, this definition is not actually valid code that you could put in your main.hs source file. But this is what the definition would look like.

data [] a = [] | a : [a]

Relative to what we’ve seen so far, this looks pretty weird! For one thing the name of this type is [] not a pronounceable word like Bool or Maybe. However, it is a type constructor like Maybe.

We’ll write it this way with some extra spacing and add some annotations to make this easier to read:

When we write it down this way, we can see that it’s actually not so different from the definition of Maybe.

  • The first thing, [], is the name the thing being defined here, and a is the name of its parameter. For example, applied [] to Integer is the type list of integer, written as either [] Integer or, more commonly, [Integer].
  • There are exactly two possibilities for the form a list value can take, separately by the | that signifies or.
    1. On one side, the [] constructor signifies an empty list, which is somewhat analogous to the Nothing constructor of Maybe.
    2. On the right side of the pipe, the (:) constructor means “an a and a list of more as.” That represents a list that has at least one item. That (:) operator is often pronounced as “cons”, and we might pronounce a : [a] as “a consed onto a list of a”.

Notice that these are what we call homogenous lists; if a has type Integer, then all of the items the list have type Integer. This is because in a : [a], the a appears twice, representing the same type in each place.

The first item in the list has the same type a as the items in the rest of the list.

We’ll see more about how to use lists in later lessons. What we want you to keep in mind for now is the similarity between these types:

data Bool    =  False   | True
data Maybe a =  Nothing | Just a
data [] a    =  []      | a : [a]

These are all examples of sum types, meaning they have two possibilities – false or true, nothing or just, empty or cons. When working with any of those types, we’re going to be using case expressions to pattern match over the two possibilities.

This is the end of the first of four parts of the beginner crash course. That was a lot to take in, especially if you’re new to programming! You may want to take a break here. In the upcoming lessons, we’ll give an extended example to see how the ideas we’ve presented so far start to make sense in the context of an actual working program.

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