Please Buy now at Leanpub
Written and illustrated by GetContented.
Published on 2017-12-25.
Feline Crescent has ten houses on it. Each house has a cat of a different breed living in it. We’ll be seeing sections of programs that we’ll slowly build up to a program that finds the oldest of the cats on the street and prints out where they live, along with their age in equivalent human years.
First, let’s see a data type for cat breeds:
data CatBreed = Siamese | Persian | Bengal | Sphynx | Burmese | Birman | RussianBlue | NorwegianForest | CornishRex | MaineCoon
If you remember, we know this sum type means we’re declaring a new type of data called
CatBreed, and all the possible values are listed above.
Next, we’ll see a data type for
Cat. This is a new kind of data type called a product type. This lets us make values that combine more than one type. When we saw tuples, you may remember we said that there are better ways to combine multiple values into one value. Well, this is that better way. A
Cat can have an
Age, and a
Name, and a breed (
type Name = String type Age = Integer data Cat = Cat Name CatBreed Age
This tells Haskell that
Cat is a new type for data, and that it has only one value constructor. Both sum types and product types are examples of algebraic data types. You can have combinations of these types in the one type, which means you can have types that are sums of product types. We’ll see more of this later, so don’t worry too much about this for now. Algebra is another fancy word which just means a sort of language of combining elements together — in this case, we’re combining types. You probably know this word from Maths if you’ve studied it.
Our code also tells Haskell that
Cat is the name of the value constructor, as well as the name of the type. So how does Haskell know when we’re talking about the type, and when we’re talking about the value? Well, by looking at the places we’re using it, it can use inference to work it out.
Cat type is defined as one single
Cat data constructor, which has variable “slots” for the name, breed and age. This actually tells Haskell to make the value constructor function
Cat :: Name -> CatBreed -> Age -> Cat and this can be used to make
Cat values (which is why it’s called a data constructor), as well as pattern match for these values. Remember how
(:) is a value constructor for the list type? Well, it’s the same thing at work here.
Next we’ll see a product type for House:
type HouseNumber = Int data House = House HouseNumber Cat
And a function to work out how old a cat is in human years:
-- this is a commonly agreed upon -- way to work out cat ages humanAge :: Cat -> Age humanAge (Cat _ _ catAge) | catAge <= 0 = 0 | catAge == 1 = 15 | catAge == 2 = 25 | otherwise = 25 + (catAge - 2) * 4
We’re using a guard pattern to match all the possibilities here. Maybe you can remember that the
(<=) operator means “is less than or equal to”. There is a similar operator for the other direction, too
(>=) which means “is greater than or equal to”.
Notice that the first argument to humanAge is a
Cat, a single value of the
Cat type, but it’s being kind of “pulled apart” by the pattern match using the value constructor. This takes the first two fields of the
Cat data type and ignores them (by matching them to “
_” which as you might know by now, basically throws them away), and then binds a variable name to the age of the
catAge, so the rest of the function can compare and do math with it.
Next we’ll see some data for a
street, which will be a list of houses, and a couple of functions for working with that data:
street :: [House] street = [ House 1 (Cat "George" Siamese 10) , House 2 (Cat "Mr Bigglesworth" Persian 5) , House 3 (Cat "Mr Tinkles" Birman 1) , House 4 (Cat "Puddy" Burmese 3) , House 5 (Cat "Tiger" Bengal 7) , House 6 (Cat "The Ninja" RussianBlue 12) , House 7 (Cat "Mr Tinklestein" NorwegianForest 8) , House 8 (Cat "Plain Cat" MaineCoon 9) , House 9 (Cat "Shnooby" Sphynx 7) , House 10 (Cat "Crazy Ears Sam" CornishRex 3) ] getCatFromHouse :: House -> Cat getCatFromHouse (House _ c) = c getHumanAgeOfCatFromHouse :: House -> Age getHumanAgeOfCatFromHouse = humanAge . getCatFromHouse
street is a value whose type is
[House]. The type of
House says it has a single data constructor, also called
House which takes two fields (
House :: HouseNumber -> Cat -> House) and returns a
House value. Each of these houses has an embedded
Cat value in it constructed with the
Cat data constructor which is building a
Cat out of a
CatBreed, and its
Moving on from there, we know that every
House must have a
Cat in it if we look at the
House type, so we see there’s a simple function that extracts the
Cat value from a
House value by using pattern matching (called
The next function (
getHumanAgeOfCatFromHouse) probably looks a little curious, because we’ve got a new operator in it named
(.) which kind of glues two functions together. It turns them into a single function that does the same as calling the first one on the result of calling the second one. It’s called the function composition operator. We’ll talk more about this function later, but you can think of it as feeding the “output” of the function on the right into the “input” of the function on the left.
The type of
getHumanAgeOfCatFromHouse is annotated to be
House -> Age. As we just saw,
getCatFromHouse takes a
House and gives a
humanAge takes a
Cat and gives an
Age. So, if we somehow could chain or pipe them together (glue them at the
Cat, so to speak!), then giving this new function a
House value would give us back an
Age. This is exactly what the
(.) operator does to two functions: it chains them together to form a new one that does the work of both, as long as they have a common type between them.
Here’s another way to write that same relation by just using regular function application rather than function composition.
getHumanAgeOfCatFromHouse :: House -> Age getHumanAgeOfCatFromHouse h = humanAge (getCatFromHouse h)
We can see that we have to include a variable for the
House value here, and we can see that we’re applying the function
getCatFromHouse to the
House, and then applying the function
humanAge to the resultant
Sometimes it makes more sense to use normal function application, like the above:
humanAge (getCatFromHouse h) and other times it makes more sense to use function composition like this:
humanAge . getCatFromHouse, but they mean the same thing. We’ll see more of
Now we’ll see a function from the
Data.List module called
find that can be used to get a particular item from a List. A module is a kind of named package of additional functions and types that other programmers have written. So, the
Data.List module contains lots of good things to do with lists, and it needs to be imported in our file at the top. We’re “aliasing” it (or locally renaming it) to
L by using the
as keyword. The
qualified keyword makes sure when it imports all the function names, it doesn’t load them directly into our local namespace. That way, if we already have things named the same thing as the module we’re importing, there won’t be any conflicts.
-- don't forget, this goes -- at the top of the file import qualified Data.List as L
Let’s look at the type signature for the
find function, noticing that we’ve qualified the function as being in the
L aliased namespace by putting
L.find instead of just
import qualified Data.List as L L.find :: Foldable t => (a -> Bool) -> t a -> Maybe a
We can see this function takes two arguments; firstly a function of type
a -> Bool, and secondly a value of type
Foldable t => t a. This function then returns another wrapper type. This one is called
Maybe, and it is wrapping the same type that our
Foldable t => t is wrappering.
What does this function do, though? Well, it takes that function of
a -> Bool and applies it to each of the items in the
Foldable t => t a. If it can’t find any that return
True, it returns the
Nothing value, which comes from the
Maybe a type. If it does find any that return
True, it returns the first one, wrapped in the
Just value constructor from the
Maybe a type.
Here’s an example:
names = ["Harry", "Larry", "Barry"] result1 = L.find isHarry names where isHarry name = name == "Harry" -- result1 will be: -- Just "Harry" result2 = L.find isJake names where isJake name = name == "Jake" -- result2 will be: -- Nothing
So we can easily see when it finds a value because
True was returned by the finding function (also called the predicate, remember?), it returns it wrapped it in
Just, and when it doesn’t, it returns
Let’s put this to work on a function that can find the oldest cat in a list of houses:
findOldestCat :: [House] -> Maybe Cat findOldestCat  = Nothing findOldestCat houses = Just oldestCat where oldestCat = getCatFromHouse houseWithOldestCat houseWithOldestCat = head housesSortedByCatAge housesSortedByCatAge = L.sortBy catAgeComparer houses catAgeComparer (House _ (Cat _ _ age1)) (House _ (Cat _ _ age2)) = compare age2 age1
This function might look insane at first, but it’s just layers on layers, and we’re going to slowly pull the layers apart together. It’ll be fun, let’s start.
Let’s go from the bottom up.
catAgeComparer is a function that takes two houses and compares the ages of the cats contained within. It does this by pattern matching the ages out of the cats, and the cats out of each house all at once (its type is
House -> House -> Ordering).
Ordering is a built-in sum data type which has values of
GT which stand for less than, equal to and greater than respectively.
Ordering values are used by sorting functions in Haskell.
sortBy :: (a -> a -> Ordering) -> [a] -> [a] function from
Data.List takes a function whose type is
(a -> a -> Ordering) to sort its second argument: a list. That fits the type of
catAgeComparer, which is why we’re using
sortBy in our definition of
housesSortedByCatAge in the line above that. Put another way, the
sortBy function takes a comparing function (that is, one that returns an
Ordering), and a list, and returns that list sorted by using the comparing function on adjacent elements.
Because we want to sort oldest to youngest, our application of the compare function in
age2 first. If
age1 was first, it’d sort youngest to oldest.
Next, we have
houseWithOldestCat which takes the
housesSortedByCatAge value obtained by sorting the houses, and picks off the first one with the
head function. It’s only safe to use the
head function when we can be absolutely sure there is at least one item in the list we’re applying it to. Otherwise it will cause your program to crash (crashing is a term that means the program unexpectedly stopped working). We’re sure that there is at least one item in the list we’re applying
head to because we have a clause that matches on the empty list and returns
oldestCat is obtained by applying
houseWithOldestCat, which will obviously just get the cat out of the house.
Now we can talk some more about the
Maybe Cat type that you can see in the
findOldestCat function’s type signature.
Maybe is a type that takes another type to make types with (this is called a type constructor). That means you can have values of type
Maybe House, or
Maybe any other concrete type you like.
It’s a sort of wrapper for other types, and it’s what we use in Haskell when we want to make a type’s value optional.
In our case, we can’t be 100% sure if the list we pass to
findOldestCat will contain something. If it is empty, then we obviously can’t pass a
Cat back at all. The way
Maybe values work is there’s a value called
Nothing which represents “no value” of the wrapped type, and there’s a value called “
Just a” which represents any other value of our wrapped type. Take a look at the following values and their type signatures:
Just 5 :: Num a => Maybe a Just "Heya" :: Maybe String Just (Cat "YOOBEY" Sphynx 8) :: Maybe Cat Nothing :: Maybe Cat Nothing :: Maybe Integer Nothing :: Maybe a
You might be surprised to know that
Nothing :: Maybe Cat can’t be compared to
Nothing :: Maybe Integer. This is because
Cat is a different type than
Integer and you can’t compare differently typed values (unless they’re polymorphic values — that is, values whose types have type variables like
Nothing :: Maybe a, or
5 :: Num a => a).
Nothing :: Maybe Cat means “there are no cats”, and
Just (Cat "YOOBEY" Sphynx 8) means “a value of the optional-
Cat type that has a
Cat in it). This is different than the values whose type is
Cat, because a value of the type
Cat must be a
Cat as defined by the type - it can’t be empty at all.
This is a very nice property for a programming language to have. If there is a value whose type is
Integer, you can be sure it won’t have anything other than exactly that in it, which makes reading and reasoning about programs much much easier.
However, every positive side has a negative side, too, and the negative side of this is that it makes working with empty things slightly more complicated than if there were just a general value that means an empty thing. We definitely think the complexity is worth it, though, because these optional types are some of the biggest sources of errors in programming languages that don’t have this feature of “typed optionality”.
We’ve added a
main function and two additional helper functions:
import qualified Data.List as L data Cat = Cat Name CatBreed Age type Name = String data CatBreed = Siamese | Persian | Bengal | Sphynx | Burmese | Birman | RussianBlue | NorwegianForest | CornishRex | MaineCoon type Age = Integer data House = House HouseNumber Cat type HouseNumber = Int street :: [House] street = [ House 1 (Cat "George" Siamese 10) , House 2 (Cat "Mr Bigglesworth" Persian 5) , House 3 (Cat "Mr Tinkles" Birman 1) , House 4 (Cat "Puddy" Burmese 3) , House 5 (Cat "Tiger" Bengal 7) , House 6 (Cat "The Ninja" RussianBlue 12) , House 7 (Cat "Mr Tinklestein" NorwegianForest 8) , House 8 (Cat "Plain Cat" MaineCoon 9) , House 9 (Cat "Shnooby" Sphynx 7) , House 10 (Cat "Crazy Ears Sam" CornishRex 3) ] humanAge :: Cat -> Age humanAge (Cat _ _ catAge) | catAge <= 0 = 0 | catAge == 1 = 15 | catAge == 2 = 25 | otherwise = 25 + (catAge - 2) * 4 getCatFromHouse :: House -> Cat getCatFromHouse (House _ c) = c getHumanAgeOfCatFromHouse :: House -> Age getHumanAgeOfCatFromHouse = humanAge . getCatFromHouse findOldestCat :: [House] -> Maybe Cat findOldestCat  = Nothing findOldestCat houses = maybeOldestCat where maybeOldestCat = case findOldestCatHouse houses of Just house -> Just (getCatFromHouse house) Nothing -> Nothing findOldestCatHouse :: [House] -> Maybe House findOldestCatHouse houses = if length housesSortedByCatAge > 0 then Just (head housesSortedByCatAge) else Nothing where housesSortedByCatAge = L.sortBy catAgeComparer houses catAgeComparer (House _ (Cat _ _ age1)) (House _ (Cat _ _ age2)) = compare age2 age1 getCatName :: Cat -> String getCatName (Cat name _ _) = name getHouseNumber :: House -> String getHouseNumber (House number _) = show number main :: IO () main = putStrLn oldest where oldest = case findOldestCatHouse street of Nothing -> "There is no oldest cat!" Just house -> "The oldest cat is " ++ getCatName (getCatFromHouse house) ++ ", is " ++ show (getHumanAgeOfCatFromHouse house) ++ " equivalent human years old" ++ " and it lives in Number " ++ getHouseNumber house
Phew! We’ve covered a lot of code in this chapter.
The two helper functions
getHouseNumber use pattern matching to grab out the name of a cat and the number of a house (and make sure they’re a string by using the
main function uses a definition called
oldest which uses a
case expression on the application of
street, which returns a value of type
Maybe House. The
case expression is used to match on either the
Nothing value to report an error, or the
Just value, in which case it takes the house out of the
Just value constructor with pattern matching and creates a nice descriptive sentence about the oldest cat, its age in human equivalent years, and where it lives.
Your homework is to write a definition for a name and value whose type is
String, and a program that prints that
putStrLn. Make sure you can do it from memory without looking it up, and write out the types for all definitions in your program.
If you’ve enjoyed reading this, please consider purchasing a copy at Leanpub today
Please follow us, and check out our videos Follow @HappyLearnTutes
Also, Volume 2 is now in beta and being written! Show your support and register your interest at its Leanpub site.