Please Buy now at Leanpub
Written and illustrated by GetContented.
Published on 2024-05-24.
In this chapter, we’ll see some code for working with our own super-simple address book, and in the process introduce an extremely useful variety of functions called higher order functions. We’ll also dig a bit deeper into sum and product types (or algebraic data types as they're generally known as together), and introduce records, another way to work with data within such data types.
Imagine you were building up a list of your favourite people from the subject areas of maths and computing. (I know, what a silly suggestion!) So, what kind of information would you want to write down about them? Let’s just pick a couple of things about them to keep track of.
A data model is a set of data types or data that describes something. We’ve actually been doing data-modelling the whole book. When we say that we’re going to use an Integer
value to represent someone’s age, for example, we’re doing data-modelling. In our case right now, we’re about to be modelling people. So, what data would make up a person?
Well, because a person can have many pieces of information about them (or we could call them fields or attributes), we need a way to build a single type out of a combination of other types. To do this in Haskell, we use a product type, as we’ve briefly seen before. Here’s an example of one of these product types:
type Name = String
type Year = Int
data Person = Person Name Name Year
deriving (Show)
You’re probably wondering about some things here. What is “deriving”? Why is data
being used without the |
symbol, and why is Name
written twice? We’ll go through this now.
Firstly, we can see that we have a type alias for Name
as String
. We also have one for Year
as Int
. Great, there’s nothing new there. We know that just says these different type names can be used for those types, and Haskell will know what we mean. As you know, these are called type aliases or type synonyms.
What about data Person
though? Well, this is how we define Person
to be an algebraic data type, as we mentioned above. The data
keyword tells Haskell we’re creating our own fresh new type. We’ve seen these before, but let’s go through it in more detail to understand it better.
The part to the left of the =
symbol is the type name. Once our type is created, this name can be used in places where types can go, for example in type annotations. This name is Person
in the example above. If we’d written data Muppet = HappyMuppet Name
instead, then Muppet
would be the type name instead.
Next we’ll look to the right of the =
symbol. We see Person
again. This is a value constructor. When we create a type like this, Haskell creates us a function (or just a value if there are no type fields) for each of the values the type describes. In our case, we just have the one, which is Person
. Looking to the right of this, we can see the types that make up this Person
value, and the value constructor.
If we’d created the more complicated data type of data SizedPerson = TallPerson Name | ShortPerson Name
, then we would have two value constructors: TallPerson :: String -> SizedPerson
and ShortPerson :: String -> SizedPerson
, just to show you how it looks when there are sum and product types in the one algebraic data type.
Anyway, it’s called a product type because a single piece of this type of data is a product of more than one piece of data of these types. These pieces of data are often called fields of the data type. Again, if we’d written data Muppet = HappyMuppet Name
, then HappyMuppet
would be the value constructor (also sometimes called the data constructor).
So, after we’ve defined this type, Haskell will have defined a new value constructor function for us automatically Person :: Name -> Name -> Year -> Person
. Notice that the return type of this function is Person
as well as the function being named Person
. If we go back to our muppet example as a contrasting example, we can see that the type of the data constructor would be this instead: HappyMuppet :: Name -> Muppet
.
There is also this deriving (Show)
line after Person
. That is for us to tell Haskell that we’d like it to create an easy to print version of this data type for us, automatically creating an instance of the Show
typeclass for this data type. When we use show
on it, it will just print it out like it’s written in the code.
Back to Person
, though, those Name
fields... why two names? Is it first and last names? If so, which is which? Let’s see an entry for a person. Maybe that will clear it up:
-- the famous mathematician
-- Blaise Pascal
blaise :: Person
blaise = Person "Blaise" "Pascal" 1623
Ah, so the given name comes first, and the family name goes second. But, what is Year
supposed to represent here? Is it the year of birth? of death? of something else?
Don’t you wish there was some way we could see exactly what the types were supposed to mean inside of the product type itself? Well, it turns out there is. It’s what’s called record syntax, which is simply a way to let us name the fields as we specify a data type. Here’s what the Person
type looks like using record syntax:
type Name = String
type Year = Int
data Person = Person
{ personFirstName :: Name
, personLastName :: Name
, yearOfBirth :: Year }
deriving (Show)
Okay, there are names for the fields now, so it’s clearer what each means. Also, Haskell automatically makes what is called a getter function for the fields, and it also allows us to use something we’ll see called record update syntax for making new data based on existing data. So, personFirstName
is a function of type Person -> Name
, which means we pass it a Person
and it gives us back the first name, and so on for the other fields. How about constructing a Person
now? What’s that like? Well, it can work like this:
blaise :: Person
blaise =
Person { personFirstName = "Blaise"
, personLastName = "Pascal"
, yearOfBirth = 1623 }
However, we can still build a Person
the usual way, and all the old things work as before.
Something new, though, is that we can also easily build new records out of others by doing the following:
traise :: Person
traise = blaise { personFirstName = "Traise" }
This is called record update syntax. This one creates a person whose data looks like this: Person {personFirstName = "Traise", personLastName = "Pascal", yearOfBirth = 1623}
. Note that we can now “set” and “get” the data for fields in any order we like. We can set more than one field at once, too.
Let’s look at some more people:
people :: [Person]
people =
[ Person "Isaac" "Newton" 1643
, Person "Leonard" "Euler" 1707
, Person "Blaise" "Pascal" 1623
, Person "Ada" "Lovelace" 1815
, Person "Alan" "Turing" 1912
, Person "Haskell" "Curry" 1900
, Person "John" "von Neumann" 1903
, Person "Lipot" "Fejer" 1880
, Person "Grace" "Hopper" 1906
, Person "Anita" "Borg" 1949
, Person "Karen" "Sparck Jones" 1935
, Person "Henriette" "Avram" 1919 ]
Let’s see how we’d find a particular person, say, the first person whose birthday is after 1900 in the list. First, we need to make sure we’ve imported the Data.List
module, because we’ll be using the find
function from that module. At the top of our code file, we make sure the import is present:
import qualified Data.List as L
Then, we can find our person with this definition of an expression:
firstAfter1900 :: Maybe Person
firstAfter1900 =
L.find (\(Person _ _ year) -> year >= 1900) people
The find
function has the following type signature L.find :: Foldable t => (a -> Bool) -> t a -> Maybe a
. This means it takes two arguments: a function from some type of value called a
to a Bool
known as the predicate, and a Foldable
of that same a
type. Our Foldable
instance will be on list, because we have people :: [Person]
as our Foldable t => t a
value.
Essentially what the function does is apply the predicate to each of the items until one returns True
in which case it returns that particular item wrapped in Just
, otherwise it returns Nothing
. It’s not the most efficient way to find things because of the way lists are constructed, but it will be fine for our purposes here.
Also to note here is the way we're using the Person
constructor to pattern-match out the parts of the Person
as it gets fed into the find
function, with our predicate: (Person _ _ year) -> year >= 1900)
. The two underscores simply throw those particular fields away because we’re not interested in them, and we just match out the year as the variable year
which we then compare to 1900
.
Instead of doing it like that, we could also have written it like this, which is a bit more flexible because it doesn't depend on the field ordering in the data type:
firstAfter1900' :: Maybe Person
firstAfter1900' =
L.find (\person -> yearOfBirth person >= 1900) people
Let’s see how we’d find the sub-list of people whose name begins with L, using recursion and the list above:
firstNameBeginsWithL :: Person -> Bool
firstNameBeginsWithL p =
case personFirstName p of
'L':_ -> True
_ -> False
makeNewListWithOnlyLPeople :: [Person] -> [Person]
makeNewListWithOnlyLPeople [] = []
makeNewListWithOnlyLPeople (x:xs)
| firstNameBeginsWithL x =
x : makeNewListWithOnlyLPeople xs
| otherwise =
makeNewListWithOnlyLPeople xs
peopleThatBeginWithL =
makeNewListWithOnlyLPeople people
The firstNameBeginsWithL
function takes a Person
as the variable p
, gets the first name with the personFirstName
getter function, then we have a case
expression on that.
If the first name is a String
beginning with the letter L
, it will match 'L':_
because (:)
is, as we know, a value constructor that matches any list and pattern-match splits it into its head and tail. This returns True
, otherwise the “_
” pattern will pattern-match on anything else, which means we return False
.
Next we’ll look at the makeNewListWithOnlyLPeople
function, which is a reasonably simple recursive function using guard patterns. Remember that guard patterns work by Haskell matching the first expression that evaluates to True
, then returning the expression on the right of the corresponding =
symbol. We pull the Person
list into head and tail as x
and xs
respectively using pattern-matching again. If the Person
in x
has a first name that begins with L
, we add it to the return list by using (:)
to prepend it to the tail of the list (xs
) with the function applied to it. If it doesn’t begin with L
, we simply apply the function to the tail of the list.
You might notice that makeNewListWithOnlyLPeople
is using the firstNameBeginsWithL
function as a kind of testing function. This type of function is called a predicate function in programming. It checks if something is true or not. What if we wanted to be able to swap out that function, and make a whole lot of different lists with people whose names started with letters other than L
? Well, next we’ll look at a general way to do just this.
You might be thinking that this way of finding something is very inefficient. You’d be correct if you were thinking this! For small amounts of data, the list type is very handy and useful, and more than efficient enough. However, for much larger amounts of data, we would want to use different functions and types if we wanted things to be fast and efficient. We’ll see these in later volumes. As with everything, the context gives meaning to the content, so as the content changes (you get a bigger set of data), we must choose different ways of working with it (choose a different context of functions and data types).
Lists are very good for certain things, such as for representing data that is added to at the front. They are also quite easy to write code for, so they’re good for beginners to look at first, such as yourself. As you learn programming more you’ll start to get an appreciation and understanding of the different types for storing data, and when it’s good to use each.
What we just saw is a very common pattern in Haskell: we take a testing function that returns a Bool
value (otherwise known as a predicate) and a list of the same type of items that the predicate takes, and we return a new list of items that resulted in True
when “tested” against the predicate. It’s so common that there’s already a built-in higher-order function for it in Haskell called filter
:
filter :: (a -> Bool) -> [a] -> [a]
A higher-order function is a function that takes one or more functions as argument(s). This term also applies to functions that return functions, but because of the way functions work in Haskell, that’s so common and easy that we usually don’t count it.
Let’s see how rewriting our “only L people” function using filter
simplifies it:
makeNewListWithOnlyLPeople' :: [Person] -> [Person]
makeNewListWithOnlyLPeople' xs =
filter firstNameBeginsWithL xs
Here, xs
is matched to the list of type Person
, and we pass it to filter
, along with our predicate (firstNameBeginsWithL
). This version does exactly what our previous function does, but with a lot less code to read and write. Using these built in common function like filter
and the others we’ll see later is really useful because it saves us having to reinvent the wheel each time we want such common functionality. It also gives us a common language to talk about these things with other Haskell programmers.
We can further simplify the definition by removing the xs
from both sides of the equals sign!
makeNewListWithOnlyLPeople'' :: [Person] -> [Person]
makeNewListWithOnlyLPeople'' =
filter firstNameBeginsWithL
That is, filter
usually takes two arguments: a function of type (a -> Bool)
(any type at all to Bool
), and a value of type [a]
(a list of that same type), then returns a value of type [a]
(another list of that same type). If we were to supply it with both arguments, it would return us value of type [a]
, but if we only supply the first one (the predicate from a -> Bool
), then we’ll end up with a function from [a]
to [a]
!
The technical name for this process of getting rid of these variables that are repeated on the right hand side of the inside and outside is called eta reduction.
Here’s another example of it:
plus num1 num2 = num1 + num2
-- the second argument "num2" gets "erased":
plus' num1 = (num1 +)
-- this is the same as:
plus'NonSectioned = \num1 -> (+) num1
plus'' = (+)
Note that (num1 +)
is technically actually what's called a section: that is, a partially applied operator. However, because we're showing this eta reduction with the (+)
operator, plus' num1 = (num1 +)
is effectively the same function as plus' num1 = (+) num1
.
All three of these functions work in the same way. The (+)
function already takes two arguments, which as we know in Haskell means it is actually two nested functions. Let’s look at yet another way to do the same thing, this time with lambdas:
add = \x -> (\y -> x + y)
-- the second argument, "y" gets "erased":
add' = \x -> (x +)
-- this is the same as:
add'NonSectioned = \x -> (+) x
add'' = (+)
In each step, we’re simply removing one of the unnecessary variables from our function definition, because (+)
is already a function itself, so by the end, all we’re doing is effectively saying that add''
is simply the (+)
function.
Getting back to our original functionality, let’s look at a different way to write the whole function:
-- don't get confused, c is not the letter c here
-- it's a variable name, holding the Char value
-- we're matching on
firstLetterIs :: Char -> String -> Bool
firstLetterIs c "" = False
firstLetterIs c (x:_) = c == x
firstNameBeginsWith :: Char -> Person -> Bool
firstNameBeginsWith c p =
firstLetterIs c firstName
where firstName = personFirstName p
peopleThatBeginWithL :: [Person]
peopleThatBeginWithL =
filter (firstNameBeginsWith 'L') people
We have firstLetterIs
, a more general function that takes a Char
and a String
and returns True
if the first letter of the String
is the passed in Char
value. The beauty of this function is if we decide we want to use a different letter, we just have to change the one spot in the code.
Then there’s the firstNameBeginsWith
function that gets the first name of the passed in Person
and matches its first letter against a passed in Char
value by using the firstLetterIs
function.
Finally, we use filter
along with the partially applied function firstNameBeginsWith 'L'
and the people list to create a Person
list value defined as peopleThatBeginWithL
.
It might be pretty clear to you now how we can easily build a list by filtering on the first name beginning with any character we like, and it should be reasonably easy to see how you could create a list of people whose last names start with a different letter. (For example, filter (firstNameBeginsWith 'H') people
).
Now we’re going to take a look at something we’ll need later on. We may want to get the last name of a Person
. We know how to do this for one Person
just fine. Let’s say the person is blaise
, then we’d write personLastName blaise
. That’s pretty straightforward.
Well, what if we actually wanted to get a whole list of first names from a whole list of people? What would we use? Well, given what we know about recursion, we’d probably write it something like this:
peopleToLastNames :: [Person] -> [String]
peopleToLastNames [] = []
peopleToLastNames (x:xs) =
personLastName x : peopleToLastNames xs
Study this little function well! What we’re seeing is a function with two definitions, as usual. It’s looking like some standard recursion. The first definition simply says if the [Person]
passed in is the empty list of Person
, return the empty list of String
.
The second definition is where most of the work takes place. This first pattern matches the head of the list into x
and the tail into xs
. So, x
will be a Person
, and xs
will be a [Person]
. It then returns applying personLastName
to x
which gives us a String
, then prepends this using (:)
to the result of calling the whole function again on the tail of the list (recursively).
Next we can imagine what it’d be like if we wanted a kind of general function so we weren’t locked in to only using the personLastName
function. Let’s see what an equivalent first name function would be like, first:
peopleToFirstNames :: [Person] -> [String]
peopleToFirstNames [] = []
peopleToFirstNames (x:xs) =
personFirstName x : peopleToFirstNames xs
Not much changes, does it? We’ve really only changed the function that gets called on each Person
value to turn it into a String
value. What if we made a general function and let the programmer using the function pass in their own function of type Person -> String
, that way we could make this quite general:
mapPeople :: (Person -> String) -> [Person] -> [String]
mapPeople f [] = []
mapPeople f (x:xs) =
f x : mapPeople f xs
Ok, notice we’ve added another function argument at the front — it’s f
, which is the function of type Person -> String
we’re passing in — and all our function definitions now have an added f
parameter and variable. Also notice we’re using it by appling it to x
before using (:)
with our recursive function application of mapPeople
at the end of the last line which also has to have the f
argument, as it’s now a required argument to our function.
This is now quite general. We can use this with first names or last names. Let’s see the redefined version of these functions now:
peopleToLastNames :: [Person] -> [String]
peopleToLastNames people =
mapPeople personLastName people
peopleToFirstNames :: [Person] -> [String]
peopleToFirstNames people =
mapPeople personFirstName people
That’s starting to look very nice and compact. We now know, though, that we can eta reduce these functions by getting rid of the people argument, as follows:
peopleToLastNames :: [Person] -> [String]
peopleToLastNames = mapPeople personLastName
peopleToFirstNames :: [Person] -> [String]
peopleToFirstNames = mapPeople personFirstName
Nice. However, it’s time to let you in on a secret. The mapPeople
function already exists in Haskell, as an even more general function called map
. This one works on lists of any type of value at all.
Let’s see its type signature:
map :: (a -> b) -> [a] -> [b]
This takes two arguments: a function from anything a
to anything b
, a list of those a
values, and returns a list of those b
values. For us, this means we'd want these a
and b
values to be Person
and String
(we call this specialisation). Pay careful attention and note that where Haskell has written a
and b
in type signatures, that doesn’t mean that those types have to be different, only that they can be different, if you’d like.
To illusrated this, if you want to map
from String
to String
, or the same type to the same type, there’s nothing stopping you. For example, here’s a function that maps from a list of strings to their reverse string counterparts, using the reverse
function: reverseMap = map reverse :: [String] -> [String]
.
So, anyway, we could have just written our mapPeople
function like this:
mapPeople :: (Person -> String) -> [Person] -> [String]
mapPeople = map
Or, we could just have used map
instead of mapPeople
.
So, given we have a list of people called people
, we’d create a list of their last names like this:
lastNames :: [String]
lastNames = map personLastName people
Very nice and compact.
So, the higher order function map
takes a function and a list. It gives a new list with the function applied to each element of the list. Let’s see how map
could be implemented:
map' :: (a -> b) -> [a] -> [b]
map' f [] = []
map' f (x:xs) = f x : map' f xs
As you can see, it’s very similar to the function we had for getting the last names of people.
Here’s a picture that might help you understand what map does visually:
You get a whole new list with new items that have been created by applying the function to the original elements. The original list and items are still there, unchanged.
This is important: Haskell values are almost never modified, they’re always created afresh, or referred to if they’re identical.
You might be tempted to think of it a bit like the function is “doing something” to each element, especially with these higher order functions, but it’s not, it’s looking at the original element and using the passed-in mapping function to make an entirely new element for the new list, leaving the original untouched. This is very good, because if two functions are referring to one value, the last thing you want is for that value to be changed by one of the functions without the other one realising it. Luckily, this can’t happen in Haskell. This is because Haskell has purity, which means functions cannot work on things other than their arguments, and also because it has immutability which is a fancy word meaning data can’t be changed! You might worry that this would make it inefficient, but that’s not the case. In many cases it actually makes it more efficient.
Ok, so now we have our last names in our lastNames
variable, perhaps we might want to sort them. First, we need to make sure we’ve imported the Data.List
module, because the sorting functions are in that module. At the top of our code file, we make sure the following is present:
import qualified Data.List as L
Before we get to these functions for sorting, we need to know a little about the Ord
typeclass. This is for types that can be ordered. Ord
provides the functions compare
, (<)
, (<=)
, (>)
, (>=)
, min
and max
. These functions allow comparison between two values in various ways. You should investigate their types using hoogle. If you use a web search for “haskell hoogle” it will find it. It’s a very handy tool for looking at functions. There is also a sum type called Ordering
that this typeclass uses that has the values LT
, GT
, and EQ
, which represent less-than, greater-than and equal-to respectively. The compare
function returns this type, and sorting functions use the compare
function and the Ordering
type to do their work.
Right, so now we can have a definition for our sorted last names using the sort
function from the Data.List
module, whose type signature is Ord a => [a] -> [a]
. It takes a list whose element type must be instances of Ord
(for comparing and ordering, remember), and returns the sorted version of that list.
sortedLastNames :: [String]
sortedLastNames = L.sort lastNames
This will be sorted alphabetically. What if we wanted it in reverse? Well, there is a reverse
function in Haskell whose type is [a] -> [a]
that will reverse any list at all. Instead of using this, though, we’re going to see how to use a function called sortBy
that takes a function like compare
, instead.
Firstly, sortBy
has a type of (a -> a -> Ordering) -> [a] -> [a]
and compare
has type Ord a => a -> a -> Ordering
, so you’ll notice that the sort
function is effectively the same thing as sortBy compare
. To reverse the order, we can simply provide a function to sortBy
that returns the opposite Ordering
that compare would return, and we can do that by swapping the arguments to compare
:
reverseSortedLastNames :: [String]
reverseSortedLastNames =
L.sortBy (\x y -> compare y x) lastNames
This definition is more efficient than doing reverse (sortBy compare lastNames)
, because it only has to go through the list once. For our small data set, this is not going to be a problem. It would matter with a very large list, though. In that case, though, a list would probably not actually be the best data structure to use.
We can see there that we’ve used a lambda of two arguments to flip the order of compare
’s arguments. This has the intended result of a reverse-sorted list of the lastNames.
There’s an arguably better way to do this than use a lambda, though. Haskell has a commonly-used function called flip
that works with any function of two or more arguments. Pass it any 2-argument function, and it’ll return you a function that works exactly the same, but has its arguments swapped. So here’s an alternate way to write reverseSortedLastNames
:
reverseSortedLastNames' :: [String]
reverseSortedLastNames' =
L.sortBy reverseCompare lastNames
where reverseCompare = flip compare
At this point, how we get the list of firstNames
should appear as no surprise.
firstNames :: [String]
firstNames =
map personFirstName people
Ok, but what if we wanted to sort the people list itself by some field of each person? Well, Data.List
has a sortOn
function whose type is Ord b => (a -> b) -> [a] -> [a]
. It orders the new list by the result of some function a -> b
applied to each element.
Let’s say for our purposes we want to create a list of people sorted by their first names. The function personFirstName
fits the a -> b
type perfectly for our purposes, as its type is Person -> String
and we want to sort on the first name String
.
sortedPeopleByFirstName :: [Person]
sortedPeopleByFirstName =
L.sortOn personFirstName people
Now let’s see a function that takes a year
, and a person
, and works out how many years ago from that year that person was born.
yearsSinceBirthAtYear :: Year -> Person -> Int
yearsSinceBirthAtYear y p = y - yearOfBirth p
We map y
to the comparison year, and p
to the passed in person, then apply the yearOfBirth
function to the person and subtract that from the comparison year. If we wanted to get this across all the people, we could map it as a part-applied function, say for 2012
:
allYearsSinceBirthAt2012 :: [Int]
allYearsSinceBirthAt2012 =
map (yearsSinceBirthAtYear 2012) people
The type of yearsSinceBirthAtYear :: Year -> Person -> Int
means if we apply one argument to it (2012
in this case), we’ll end up with a function (Person -> Int)
that is locked to compare with 2012
, takes a single argument (a Person
) and replies with the number of years difference between 2012
and that person’s birth year.
And now a function that shows the earliest year of birth for the people on our list. This uses the minimum
function which will work on lists containing instances of Ord
. Actually it’s a very general function, because it will work not only on list, but any instance of Foldable
. There’s also a maximum
function that gets the highest ordered value, too. Let’s see their type signatures before we proceed:
minimum :: (Ord a, Foldable t) => t a -> a
maximum :: (Ord a, Foldable t) => t a -> a
These functions have two typeclass constraints on them. This says that t
must be an instance of the Foldable
typeclass, but also that a
must be an instance of the Ord
typeclass. Note that you cannot pass an empty list into these functions. You must only pass a list that has at least one item in them.
Luckily for us, Int
has an instance for Ord
, and list has an instance for Foldable
, so minimum
will work perfectly for us:
earliestYearOfBirth :: [Person] -> Year
earliestYearOfBirth people =
minimum (L.map yearOfBirth people)
In Haskell, we prefer not to use so many parentheses. There is a higher order function called ($)
that will take a function on the left of it, and some expression on the right, and apply the function to the expression. It has an extremely low precedence, which means it will pretty much be applied last of all. This is basically the same effect as having parentheses wrapped around the expression on the right. Let’s see how the function above can be written with the ($)
function.
earliestYearOfBirth' :: [Person] -> Year
earliestYearOfBirth' people =
minimum $ L.map yearOfBirth people
Note that we can’t get rid of the people
variable, though, because the minimum
function is wrapping the whole expression L.map yearOfBirth people
.
Note also that the ($)
function is only for the cases where the parentheses would go right to the very end of the expression on the right.
Last of all, we want to find out which Person
was born first out of our list of people.
bornFirst :: [Person] -> Person
bornFirst people =
L.minimumBy compareBirthYears people
where compareBirthYears x y =
compare (yearOfBirth x) (yearOfBirth y)
Lots to explain here!
Data.List
’s minimumBy :: Foldable t => (a -> a -> Ordering) -> t a -> a
function is another higher-order function: that is, it takes a function which is a comparing function (a -> a -> Ordering)
. It also takes a Foldable
instance wrapping some “a
” values of any type, and gives us the minimum one by using the comparing function on successive pairs of items.
Notice that the “a
” type doesn’t have to be an instance of Ord
here. So long as the comparing function returns an Ordering
and its two arguments are the same type, minimumBy
will compile with no problem.
Here, we’re using a where
clause to locally scope the comparing function compareBirthYears
, which simply takes two people, matches them into x
and y
respectively, and returns the application of the compare
function on their yearOfBirth
fields.
Because compare
has the type Ord a => a -> a -> Ordering
, and the yearOfBirth
fields are Integer
and they are instance of Ord
, compare can do the comparison, and this means compareBirthYears x y
returns an Ordering
, which means it’s the correct type that minimumBy
requires.
These higher-order functions such as map
, filter
, fold
and now minimumBy
and maximumBy
might seem complicated at first, but with lots of practice in thinking through all the types of the functions concerned, they will become second nature to read, and then later, to write.
Your homework is to adjust the program by adding middle name (middleName
) as a field to a Person
, and adjusting all the functions and usage of functions as you go. Make up some middle names for these people. See if you notice that by using records, we’ve made it much easier to change our program. See if you can imagine how difficult it would be changing our program if all the functions were tied to the shape of our data type!
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.