Happy Learn Haskell Tutorial Vol 1

Buy now at Leanpub

Please Buy now at Leanpub

Written and illustrated by GetContented.

Published on 2017-02-12.


Contents

Main Table of Contents
Previous chapter: 12. How To Write Programs
13. At The Zoo
... 13.1. Sum Types
... 13.2. Pattern Matching with Sum Types
... 13.3. More Recursion
... 13.4. What is Currying?
... 13.5. The Finished Program
... 13.6. Homework
Next chapter: 14. Cats and Houses

13. At The Zoo 🔗

In this chapter, we'll make a small program that tells a Zoo owner what advice to take for each animal in the Zoo if it escapes. In the process, we’ll introduce you to an awesome feature of Haskell: the ability to make your own data types, and values of those types.

We’ll have a value for each of a number of animals, and we’ll also have a list of animals which we’ll call a Zoo.

To do this, we’ll use the data keyword which creates a new data type, and specifies all the values it can possibly be. Another name for these kinds of types is a sum type, because the values of the type “summed together” make up the whole type.

13.1. Sum Types 🔗

So, let’s see a sum type.


data Animal = Giraffe
            | Elephant
            | Tiger
            | Flea

We’re saying that we want Haskell to make a new type, named Animal. We’re also saying that the data for the Animal type can be any one of Giraffe, Elephant, Tiger or Flea, but nothing else. These values are also called value constructors, even though they’re each only able to construct the value that they are. We’ll see why more later.

Let’s see the type for Zoo next:


type Zoo = [Animal]

Pretty simple. A Zoo is an Animal list. You might recognise this is just a type synonym, for our convenience and documentation. Next we’ll see a definition for a Zoo:


localZoo :: Zoo
localZoo = [ Elephant
           , Tiger
           , Tiger
           , Giraffe
           , Elephant
           ]

Ok, the Zoo named localZoo has some Animal values in it. Let’s put all of this together and add a function that uses a case expression to give some advice when a particular animal escapes.

13.2. Pattern Matching with Sum Types 🔗

Below, we have a function that takes a single Animal, and returns a piece of advice as a String, for when that Animal escapes. Looking at the case expression, you can see it’s matching against the values of the Animal data type.


data Animal = Giraffe
            | Elephant
            | Tiger
            | Flea

type Zoo = [Animal]

localZoo :: Zoo
localZoo = [ Elephant
           , Tiger
           , Tiger
           , Giraffe
           , Elephant
           ]

adviceOnEscape :: Animal -> String
adviceOnEscape animal =
  case animal of
    Giraffe   -> "Look up"
    Elephant  -> "Ear to the ground"
    Tiger     -> "Check the morgues"
    Flea      -> "Don't worry"

If you remember how case expressions work, the animal variable is checked against each of the left hand side patterns to see if it maches, and if it does, the right hand side expression (here a String value) will be returned.

Do you notice that there’s no default case, usually marked with an underscore? That’s because we know this function is total already, because it has one item for each of the possible data values of the Animal type, so there’s nothing left to catch for a default case.

13.3. More Recursion 🔗

Next we’re going to look at a function that takes a Zoo, and returns a list of all the advice for when all the animals in that Zoo escape, by using the adviceOnEscape function and recursion.


adviceOnZooEscape :: Zoo -> [String]
adviceOnZooEscape []      = []
adviceOnZooEscape (x:xs)  =
  adviceOnEscape x : adviceOnZooEscape xs

Maybe you recognise this code as recursion, and similar to the previous chapters, in that it has a kind of a “folding” shape.

It’s a little bit different, though, because we can’t just use (:) as the folding function, as we’re also applying adviceOnEscape to each item as we fold them together into the new list.

In fact, in this case while we could think of it as folding the list into another list, we’re not really folding the list down to a single value, we’re just applying a function across all the elements of the list. Another way to look at it is that we’re making a new list that is just like the old one with a function applied to all its elements.

We could try a fold to do this, but we’d have to extract both the adviceOnEscape and the (:) out into a single folding function. Let’s see what that would look like, and we’ll also see some more local binding using a where clause while we do so:


adviceOnZooEscape :: Zoo -> [String]
adviceOnZooEscape []      = []
adviceOnZooEscape (x:xs)  =
  adviceOnEscape x : adviceOnZooEscape xs

adviceOnZooEscape' :: Zoo -> [String]
adviceOnZooEscape' xs =
    foldr addAdviceForAnimal [] xs
  where addAdviceForAnimal animal adviceList =
            adviceOnEscape animal : adviceList

When we look at them together, we can see that we’re worse off that before! foldr was supposed to help us write less code, but it actually has us producing more! This should be telling us something: that foldr is not the right abstraction to use here.

We included a function called addAdviceForAnimal, which we used as our folding function. As we’ve seen before, the where clause handily makes definitions below it available to the adviceOnZooEscape’ function. This is called local scoping. The where clause makes the definitions within it only available within the expressions that appear above it. We’ll see more of this soon.

So, it turns out that there is actually a function whose job it is to do what we want here: take a list of a and turn it into a list of b, using a function of type (a -> b) (which we could call the mapping function). It’s called map :: (a -> b) -> [a] -> [b], because it keeps the “shape” of the list, but through across all its values and applies the mapping function to each item, producing a new list of mapped values as it does. Let’s see it in action:


adviceOnZooEscape :: Zoo -> [String]
adviceOnZooEscape xs = map adviceOnEscape xs

Really nice, and clean looking. The map function is called a higher order function because it takes a function as an argument, so it’s an order higher than normal functions that just take values.

Also, we can simplify this by not mentioning the argument to the function. It still has one, but it’s implied by the types of the function and map. Observe:


adviceOnZooEscape :: Zoo -> [String]
adviceOnZooEscape = map adviceOnEscape

You give a 2-argument function only 1 argument, and this turns it into a 1-argument function! Haskell rocks!

We’re defining adviceOnZooEscape as a function of one argument without mentioning the argument it takes. We can do this because we’re also not mentioning the second argument of map. Another way to say this is that we’ve made an expression of map, a 2-argument function, and we’ve only given it one of its arguments, so the result is a function of one argument.

13.4. What is Currying? 🔗

You may remember that a function plus :: Int -> Int -> Int can be defined as plus x y = x + y, or it can be defined as plus = \x -> (\y -> x + y), they’re identical to Haskell, because a 2-argument function is actually a function that returns a function of one argument. If we give plus one Int value, it will bind that value to x, and return the inner function. Let’s see a defition for plus5: plus5 = plus 5. This will return the function y -> 5 + y. This way of defining multiple argument functions is called currying. It’s named after one of the men who invented it, Haskell Curry. Yes, Haskell is named after him.

13.5. The Finished Program 🔗

Next we’ll see how this connects up to a comma-separating function, and a definition for main to finish the program, and we’ll use another where clause:


import qualified Data.List as L

data Animal = Giraffe
            | Elephant
            | Tiger
            | Flea
type Zoo = [Animal]

localZoo :: Zoo
localZoo = [ Elephant
           , Tiger
           , Tiger
           , Giraffe
           , Elephant
           ]

adviceOnEscape :: Animal -> String
adviceOnEscape animal =
  case animal of
    Giraffe   -> "Look up"
    Elephant  -> "Ear to the ground"
    Tiger     -> "Check the morgues"
    Flea      -> "Don't worry"

adviceOnZooEscape :: Zoo -> [String]
adviceOnZooEscape = map adviceOnEscape

joinedWithCommasBetween :: [String] -> String
joinedWithCommasBetween []     = ""
joinedWithCommasBetween [x]    = x
joinedWithCommasBetween (x:xs) =
  x ++ ", " ++ joinedWithCommasBetween xs

main :: IO ()
main = putStrLn stringToPrint
  where
    stringToPrint = L.intercalate ", " advices
    advices = adviceOnZooEscape localZoo

We have some new things here. Firstly, we have a qualified import. Importing is how we can include other code to use in ours. Making it qualified means all the imports actually sit underneath a special name (we’re calling it L here), and so as you can see, when we want to use the intercalate function below, in main’s definition, we have to write L.intercalate to tell it we mean the one inside the Data.List module.

The second thing to note is that we’ve included a joinedWithCommasBetween function. We’re not actually using it here. We’ve seen it before, but it’s identical to the function obtained by providing the intercalate function from Data.List with a ", " value, except that it also works on any Lists, not just [String], so we included the definition so you can understand one way intercalate could work.

The type of intercalate is [a] -> [[a]] -> [a]. Because the String type is actually just a synonym for [Char], this fits if “a” is Char. (It fits as [Char] -> [[Char]] -> [Char] which is the same as String -> [String] -> String).

The type of the expression intercalate ", " is [String] -> String, same as joinedWithCommasBetween.

We’re using two lines in a where clause inside our main, this time. There is advices, which uses adviceOnZooEscape to build a list of pieces of advice using localZoo, and stringToPrint which uses intercalate and advices to create a string that is passed to putStrLn.

13.6. Homework 🔗

Your homework is to write a program that prints out the String "Hello there", and then change it to print out your name. Try to remember what you have to write and not look at the book while you’re writing your program. Only once you’ve finished, check your work against the book, and by running the program.

Once you’ve done that, do it again, but make the program print out your mother’s name.

Once you’ve done that, do it again this time with your favourite colour.

Do this by both using a separate definition for the String, as well as putting the String directly in.

You should do this as many times as you need to with different String values, not looking until after, so that you can write any program that prints out a String without looking anything up. Make sure you’re writing the type signatures, as well.

Now it’s time to pat yourself on the back, and take a break - you’ve written your first Haskell programs! Well done.

At this point, you know how simple function application works. You take a value, and you put it to the right of a function, and this expression in total is equal to the returned value.

Why did we wait so long before recommending you begin to write code? Simply, we want you to be very comfortable with seeing, reading and understanding things you write before you begin to write them.


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.


Main Table of Contents
Previous chapter: 12. How To Write Programs
13. At The Zoo
... 13.1. Sum Types
... 13.2. Pattern Matching with Sum Types
... 13.3. More Recursion
... 13.4. What is Currying?
... 13.5. The Finished Program
... 13.6. Homework
Next chapter: 14. Cats and Houses