Please Buy now at Leanpub
Written and illustrated by GetContented.
Published on 2017-02-12.
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.
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
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
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
localZoo :: Zoo localZoo = [ Elephant , Tiger , Tiger , Giraffe , Elephant ]
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.
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.
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
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!
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.
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
Int value, it will bind that value to
x, and return the inner function. Let’s see a defition for
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.
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
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
[a] -> [[a]] -> [a]. Because the
String type is actually just a synonym for
[Char], this fits if “
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
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
stringToPrint which uses
advices to create a string that is passed to
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.