In this tutorial we shall investigate the two most fundamental composite data types, namely lists and tuples.
This exercise follows the example in Chapter 5.7 of Simon Thompson's book.
We are going to design a simple system to record books borrowed from a library. To do this, we will need both tuples and lists. If you want to, you may use algebraic data types, but if you have enough on your plate already, don't think about algebraic data types until next week.
Primarily, we need a data structure to record loans. A loan in this context is the fact that a given person has borrowed a particular book. Thus we also need to be able to represent books and persons.
You need to create a new Haskell module (file) with all the definitions in this tutorial.
It is useful to run ghci
in a window beside your editor,
and reload the file after every amendment and test functions.
Only reloading the file will tell you if it is syntactically correct.
Books and persons may be represented in different ways in terms of data types. Think through the following:
For the sake of simplicity, we use the following types to represent books and people:
type Person = String
type Book = String
In other words, we use just the name and the title to represent a person and a book.
Define a data type Loan
to represent a loan.
Note that the loan needs to record both the book borrowed and the person borrowing. What data type(s) is/are appropriate?
Finally we need a data type Database
which records the set
of outstanding loans.
What data types can be used to represent a set in Haskell?
Define a data type Database
to represent the set of all
loans.
Let's define a couple of objects for the sake of testing.
Define the following constants of type Loan in your module:
loan1
representing the fact that
"John"
has borrowed "Huckleberry Finn"
loan2
representing the fact that
"John"
has borrowed "Tom Sawyer"
loan3
representing the fact that
"Jane"
has borrowed "Tom Sawyer"
Define a database db1
containing each of
the three loans loan1
, loan2
, and
loan3
.
Check your definitions by evaluating them in ghci
.
A database is nothing without functions to access it. We start with two retrieval functions.
books :: Database -> Person -> [Book]
borrowers :: Database -> Book -> [Person]
The first function returns a list of books borrowed by the given person. The second function returns a list of person borrowing copies of the given book.
Implement the two retrieval functions.
Test both functions in ghci
:
books db1 "John"
books db1 "Jane"
borrowers db1 "Huckleberry Finn"
borrowers db1 "Tom Sawyer"
What output do you expect? What output do you get? Compare the two.
We want two more retrieval functions:
numBorrowed :: Database -> Person -> Int
borrowed :: Database -> Book -> Bool
The first function says whether the given book is borrowed or not. The second function gives the number of books borrowed by the given person.
Implement the two new retrieval functions.
Note that you can use books
and borrowers
as helpers for the two new functions.
Test both functions in ghci
:
numBorrowed db1 "John"
numBorrowed db1 "Jane"
borrowed db1 "Huckleberry Finn"
borrowed db1 "Tom Sawyer"
What output do you expect? What output do you get? Compare the two.
We are able to retrieve data, but we also want to update the database. Since Haskell does not have variables, we cannot really change anything. We need functions which take the old database as input and returns a modified database as output. We shall implement the following to functions:
makeLoan :: Database -> Person -> Book -> Database
returnLoan :: Database -> Person -> Book -> Database
The first function adds a loan to the database, and the second one removes a loan.
Implement the two modifier functions.
Test both functions in ghci
:
makeLoan db1 "John" "Oliver Twist"
returnLoan db1 "John" "Huckleberry Finn"
returnLoan db1 "Jane" "Huckleberry Finn"
What output do you expect? What output do you get? Compare the two.
What happens if you add duplicate loans? Decide what you expect first, and try it out afterwards:
makeLoan db1 "John" "Tom Sawyer"
let db2 = makeLoan db1 "John" "Tom Sawyer"
returnLoan db2 "John" "Tom Sawyer"
Answer the following:
Debugging a pure functional program can often feel like fumbling in the dark. If you have programmed imperatively, you are hopefully used to adding extra output instruction, to see what happens when the program runs.
Why don't we just add such output lines in functional programs?
The answer is of course that I/O is side effects, and we don't like side effects. We are going to learn how to do proper I/O tomorrow, but even so it is often desirable to keep core functions pure and without I/O.
However, for debugging purposes, it is possible to cheat, with the Debyg.Trace module. It provides a couple of functions which pretend to be pure, but which still produces output. This is good for debugging, but should otherwise be avoided. Feel free to look it up.
See Thompson Chapter 6.4