Please Buy now at Leanpub
Written and illustrated by GetContented.
Published on 2024-05-24.
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 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.
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 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.
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.
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
.
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.