A small report, end to end
An orders report built out of everything in the course: maps in a vector, threading with filter, sort-by and map, and one line printed per order.
What the program is made of
The data is a vector of maps: one order per map, each with an id, a name, an amount and whether it has been paid. Every tool the report needs has already appeared. filter keeps the paid orders, sort-by puts them in order, and map turns each one into a line of text. A row is a map, so every step reads its fields by name and no step needs to remember that amount was the third slot.
There is no new idea in this chapter, and that is the point. A program this size is a composition: a few small pieces you can read one at a time.
A pipeline that reads like a sentence
The whole report is one ->>: take the orders, keep the paid ones, sort by amount, turn each into a line. Read it from the top down and it says what it does, with no bracket at the end to hold in your head. The first line is the data and the last line is the answer, and each line between is one verb: keep, sort, turn into a line.
The total is the same steps with a different ending: keep the paid orders, take the amount from each, and let reduce add them up. Same shape, different last line, which is a good sign that the steps were the right size.
Printing is the last step
Building the lines first and printing them second keeps the two jobs apart. A vector of ready-made lines is easy to check when something looks wrong, and the printing is then one doseq at the bottom of the file. A line that is built can be inspected before it is printed, and a line that is printed has already been decided; the order keeps the decision visible.
That is the shape most small programs take: data at the top, one function per step, a pipeline in the middle, and the writing-out last. Nothing about it needs to change as the program grows.
The data is already a table
The report’s data is a vector of maps, which is a table written out: the vector is the rows and the keys are the columns. Once you see it that way, each step is a table operation — filter chooses rows, sort-by orders them, map edits a column. The vector is the row order and the map is the column names, which is why sorting the vector reorders the table without touching a row.
That model is why the code has no indexes or counters. A row already carries its fields by name, so a step asks for :amount rather than remembering that amount was the third slot. A row that carries its own names also survives a change of shape: add a field and every step that does not ask for it keeps working.
Stable output is part of correctness
A report that prints its rows in a different order on each run is hard to trust. Sort before printing, and sort on a key that cannot tie — an id, or a name plus an id — so the reader sees the same thing every time. An id is the safest tiebreaker because it is unique; a name plus an id is the next best when the id is not shown.
Keep the calculation separate from the display. A total computed in the same expression that prints it cannot be reused, and a line built before it is printed can be counted, stored or tested. The extra name is cheaper than the second copy of the logic. The same rule applies to the total: compute it once, print it once, and let both the report and the summary use the same function.
The report and its total
(def orders [{:id 1 :who "Ada" :amount 120 :paid true}
{:id 2 :who "Alan" :amount 80 :paid false}])
(defn line-for [o]
(str (:who o) " paid " (:amount o)))
(def lines
(->> orders
(filter :paid)
(sort-by :amount)
(map line-for)))
(doseq [line lines]
(println line))The orders report
Run it and read the three lines. Then add a fourth order, print the unpaid ones as well, and add a line at the bottom that prints how many orders there are in total.
Read the output, then break the program on purpose: change the filter to keep the unpaid orders, or drop sort-by and watch the order change. A program you have taken apart once is a program you understand.
Three end-of-course traps. Filtering after mapping usually fails, because the map has already turned the row into text and the key you wanted is gone. Mixing -> and ->> in one chain puts the value in the wrong position; split the chain if the steps disagree. And relying on a map’s printed order makes the report change between runs, even though the data is the same.
Try these next
- Add a fifth order and an unpaid row; check both the report and the total ignore it.
- Change the report to print the biggest order first and confirm the total is unchanged.
- Group the orders by customer and print one total per customer, sorted by name.
A stock report from the same pieces
Run it, then add an item with zero quantity and check it appears in no line and adds nothing to the total. Then print the total value per category.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does (filter :paid orders) keep?
