Happy Learn Haskell Tutorial Vol 1

Buy now at Leanpub

Please Buy now at Leanpub

Written and illustrated by GetContented.

Published on 2024-05-24.


Main Table of Contents
Previous chapter: 16. Fridge, The Game
17. The People Book
... 17.1. Models of Data
... 17.2. More on Data Types
... 17.3. Making Our Types Show
... 17.4. Building Our First Value
... 17.5. Records
... 17.6. Finding a Person from the List
... 17.7. Filtering out People in a List
... 17.8. A Note About List Efficiency
... 17.9. Higher Order Functions: filter
... 17.10. Some Eta Reduction
... 17.11. Using filter
... 17.12. Higher Order Functions: map
... 17.13. Higher Order Functions: sortBy
... 17.14. Removing Parentheses With The ($) Function
... 17.15. Using minimumBy
... 17.16. Homework
Next chapter: 18. Times-Table Train of Terror

17. The People Book 🔗

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.

17.1. Models of Data 🔗

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.

17.2. More on Data Types 🔗

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.

17.3. Making Our Types Show 🔗

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.

17.4. Building Our First Value 🔗

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?

17.5. Records 🔗

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 ]

17.6. Finding a Person from the List 🔗

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

17.7. Filtering out People in a List 🔗

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.

17.8. A Note About List Efficiency 🔗

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.

17.9. Higher Order Functions: filter 🔗

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.

17.10. Some Eta Reduction 🔗

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.

17.11. Using filter 🔗

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).

17.12. Higher Order Functions: map 🔗

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.

17.13. Higher Order Functions: sortBy 🔗

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)

17.14. Removing Parentheses With The ($) Function 🔗

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.

17.15. Using minimumBy 🔗

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.

17.16. Homework 🔗

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.

Main Table of Contents
Previous chapter: 16. Fridge, The Game
17. The People Book
... 17.1. Models of Data
... 17.2. More on Data Types
... 17.3. Making Our Types Show
... 17.4. Building Our First Value
... 17.5. Records
... 17.6. Finding a Person from the List
... 17.7. Filtering out People in a List
... 17.8. A Note About List Efficiency
... 17.9. Higher Order Functions: filter
... 17.10. Some Eta Reduction
... 17.11. Using filter
... 17.12. Higher Order Functions: map
... 17.13. Higher Order Functions: sortBy
... 17.14. Removing Parentheses With The ($) Function
... 17.15. Using minimumBy
... 17.16. Homework
Next chapter: 18. Times-Table Train of Terror