Putting it together: a sales report
A capstone that uses most of the course: orders as maps, one foldl that aggregates them by item, a sorted list of rows, money rounded to two places, and a report with a footer.
The shape of the program
The data is a list of orders, and each order is a map with an item, a quantity and a price. One fold turns that list into a map from item to total value, which is the aggregation step. The result is turned into a list of pairs so it can be sorted, and each pair prints as one formatted row.
Notice how little code is doing the work. The data is a list, the aggregate is a fold, the ordering is a sort, and the printing is one format string. There is no state being carried between steps, because each step returns a value that the next step consumes.
Folding into a map
lists:foldl(fun(Key, Acc) -> add(Key, Acc) end, #{}, Os) starts with an empty map. For each order, add reads the item with maps:get, looks up what that item has earned so far with maps:get(Key, Acc, 0.0), and writes the sum back with Acc#{Key => Sum}. The third argument of maps:get is the default for an item seen for the first time, which is exactly what a running total needs.
The result is a map, and a map has no order worth relying on, so maps:to_list turns it into pairs and lists:sort puts them in order by item name. Sorting is how a report states the order it reads in; without it the output would be an accident of how the map happens to be stored.
Money, and where a real project goes next
There is no currency type in Erlang, so money is a decimal fraction and the formatting is in the format string. ~8.2f prints a fraction in a field eight characters wide with two decimal places, right-justified, and ~-6s prints a string in a field six wide, left-justified. Between them the columns line up and the pennies do not wander.
The orders would come from somewhere real — a file, a database, a queue — and each of those is one more module that turns the outside world into this list of maps. The report would be tested, which for a functional program means calling the functions with known lists and checking the answers. Neither step asks for a new idea: another module, another function, the same patterns.
The whole capstone, one page
-module(prog).
-export([main/1]).
main(_) ->
Os = [
#{item => "desk", qty => 1, price => 249.0},
#{item => "map", qty => 3, price => 19.0},
#{item => "lamp", qty => 2, price => 24.5},
#{item => "map", qty => 2, price => 19.0}
],
Totals = lists:foldl(fun(Key, Acc) -> add(Key, Acc) end, #{}, Os),
Rows = lists:sort(maps:to_list(Totals)),
Sum = lists:sum([V || {_, V} <- Rows]),
io:format("item value~n"),
lists:foreach(fun({I, V}) ->
io:format("~-6s~8.2f~n", [I, V]) end, Rows),
io:format("orders ~p total ~.2f~n", [length(Os), Sum]).
add(O, Acc) ->
K = maps:get(item, O),
Was = maps:get(K, Acc, 0.0),
Acc#{K => Was + maps:get(qty, O) * maps:get(price, O)}.Four orders, three items, one page
Run it and check the three rows against the orders by hand. Then add an order with a new item, and print the number of distinct items with `map_size`.
A capstone is a reunion, not an exam: maps, a fold, a sort, a formatted row, a fun passed to lists:foreach and a fun that calls add/2 are all earlier chapters. If a line looks unfamiliar, the chapter it came from is one page back.
A report is a function from data to text
The cleanest report has two halves: one function turns the data into rows, and another prints them. If the rows are values, the report can be checked without a screen, and the printing stays a single lists:foreach at the edge. That separation is what makes a capstone testable rather than something you can only look at.
The same idea applies to money and measurements. Keep the numbers as numbers through the fold, the sort and the arithmetic, and format them only when a row is built. Rounding early loses information that a later total may need, and a string cannot be added to anything.
A map is an index, a list is a report
A map is the right shape while the program is aggregating, because it answers “what is the total for this item?” without a search. A list is the right shape when the answer is read in order, because it can be sorted. The capstone uses both on purpose: fold into a map, then turn it into pairs and sort those pairs.
That is why maps:to_list appears before lists:sort. The map has no order to rely on, and the report has to state one. Sorting by the key is a choice the program makes, not something the map was storing.
Three capstone mistakes are worth catching before they reach the output. The fold starts with an empty map, so the first order for an item needs the three-argument maps:get with a default. A map has no order, so printing it without turning it into a sorted list makes the rows move. And floats are approximations: compare totals with a tolerance, or keep the arithmetic in integers, rather than expecting two computed prices to be exactly equal.
Try these next
- Add a second report that counts orders rather than summing their value.
- Change the sort so it orders by total value instead of by item name.
- Build the rows in a function and print them from another, then check the rows without printing.
A second report: hours by project
Run it and check each project total against the entries. Then add an entry for `atlas` and one for a new project, and confirm both the rows and the total change.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
Why does add use maps:get(Key, Acc, 0.0) instead of the two-argument form?
