Capstone: a sales report from records
A report built from everything in the course: records with a derived order, a sort by one field, totals that aggregate, and formatted lines printed by the map that runs an action.
The record
The program keeps its sales in a list of values of a type it defines itself, one field for the amount, one for the seller and one for the team. Field order is not decoration: deriving (Ord) compares the fields left to right, so the amount decides the order and the other two only break ties.
One small function turns a row into a report line. Keeping that in one place is what makes the printed report change in one edit later, rather than in five.
Sorting by a field
Sorting does not come for free here. The usual sort lives in a module that this page does not load, because files in this course carry no imports, so the sort is written out by hand.
The version below asks a simple question about each row: how many rows are smaller than it? That count is its position, counting from zero, so walking the positions in order and picking the row that belongs at each one produces the sorted list. It is short, it uses only what the course has taught, and it is easy to check by hand on a list of three.
That sort is quadratic: for every row it walks the whole list, so a hundred rows do about ten thousand comparisons. For the three rows of a report the cost is invisible, and for a list that arrives from a file it is not. The usual sort in the standard library does far less work, which is why real programs import it.
The hand-written version is here because the course has no imports and because the idea is worth seeing once: a position is a count of what comes before it. When you meet the library sort later, the comparison function will be the only part you have to write, and you will already know what it has to answer.
Totals, and the report
The report has two halves. The rows come from mapping the formatting function over the sorted list and then printing each line in turn with the map that runs an action, the name with the trailing underscore in the example below. The totals are aggregations: a sum over every sale, and a sum over one team, each written as a comprehension that picks the amounts it needs.
Aggregating by a field is the last new thing here, and it is the same idea as the totals in the chapters before it: choose the values with a pattern, then fold them into one number.
A record and a count of smaller rows
data Item = Item String Int
deriving (Show, Eq, Ord)
smaller :: Item -> [Item] -> Int
smaller i xs = length [j | j <- xs, j < i]
names :: [Item] -> [String]
names xs = [n | Item n _ <- xs]Three sales, one report
Run it and check the order of the three rows by hand. Then add a fourth sale on the south team and predict both team totals before you run it again.
Two ideas did all the work in this program: a type that carries its own comparison, and a comprehension or fold that turns many values into one. A report is only ever those two ideas arranged for a reader, which is why the whole program fits in a screen.
The report is a pipeline
Every report in this course has the same four stages: a list of records, an order, a shape for one line, and a way to print them all. Keeping the stages separate is what makes the program readable: sortByYear knows nothing about text, and line knows nothing about order.
The types at the joins are the documentation. [Book] -> [Book] is a reordering, Book -> String is formatting, and [String] -> IO () is printing.
The totals are the same idea on a different axis: a comprehension picks the field, and sum, minimum or length folds it to one number.
Sort by a key, not by the value
A derived Ord compares every field, in the order the constructor declares them. That is the right order when the record itself is the thing being ordered, and the wrong one when the report is about a single field.
Sorting by a key means comparing year a <= year b and ignoring everything else. The sort does not need to know that Book has a title, and two books with the same year keep whatever order the algorithm gives them.
Insertion sort is the smallest sort that shows the shape: sort the rest, then put the first value where it belongs. It is easy to check by hand on a shelf of three.
minimum, maximum and head all stop the program on an empty list, so a report over data that may be empty needs a case for [] before it asks. A derived order on a record sorts by the first field, which may not be the field the report is about. And printing a record with show gives the constructor and every field, not a report line.
Try these next
- Add a fourth book and check the sorted order and the page total by hand.
- Sort by title instead of year by changing one comparison, and leave the rest of the program alone.
- Add a line that prints the average page count, using
divandlengthinstead of a decimal.
A shelf sorted by its year
Run it and read the six lines. Then add a fourth book published in 1979 and check where the sort places it and what the count before 1980 becomes.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
In data Sale = Sale Int String String, which field does the derived order compare first?
