The example of the supermarket bill:
Here is a start for our file. We will be using HUnit tests to test the program. We explicitly import the prelude while hiding the particular lookup
function, as we will implement our own.
module Billing where
import Test.HUnit
import Prelude hiding (lookup)
tests = TestList [
-- will add tests here
]
We can run the tests in the file via:
:load Billing
runTestTT tests
Now let’s set up some types, a sample database and a special “unknown item”:
type Name = String
type Price = Int
type BarCode = Int
type Database = [(BarCode,Name,Price)]
type Till = [BarCode]
type BillItem = (Name, Price)
type Bill = [BillItem]
codeIndex :: Database
codeIndex = [ (4719, "Fish Fingers" , 121),
(5643, "Nappies" , 1010),
(3814, "Orange Jelly", 56),
(1111, "Hula Hoops", 21),
(1112, "Hula Hoops (Giant)", 133),
(1234, "Dry Sherry, 1lt", 540) ]
unknownItem :: BillItem
unknownItem = ("Unknown Item", 0)
We also fix a value for the length of the bill report lines:
lineLength :: Int
lineLength = 30
Now we can think of breaking our problem up in functions that perform the various steps. In functional programming languages one important step is to consider functions that produce intermediate results, then compose those functions. In our case we can start with a Till
value and use it to produce a Bill
value. Then we can separately worry about turning that Bill
value into a printable string. So at a high level our program can be decomposed thus:
produceBill :: Till -> String
produceBill till = formatBill (makeBill till)
-- Shortcut for this: produceBill = formatBill . makeBill
makeBill :: Till -> Bill
makeBill till = [] -- Need to fix this
formatBill :: Bill -> String
formatBill bill = "" -- Need to fix this
We put some dummy implementations for now.
This function composition is one of the tools at our disposal for breaking down a complex problem into steps.
Another common pattern is that of a list transformation. Our list comprehension work is great for that. In our case, we can implement makeBill
by looking up each code in the till into the database. We can offload the work of that lookup to another functions, which we will call lookup
:
makeBill till = [lookup code | code <- till]
lookup :: BarCode -> BillItem
lookup code = look codeIndex code
look :: Database -> BarCode -> BillItem
look db code = unknownItem -- Need to fix this
Now the look
function has to do some real work. We will use two tests for it, and add them to the tests
list from earlier:
tests = TestList [
TestCase $ assertEqual "lookExists"
("Orange Jelly", 56)
(look codeIndex 3814),
TestCase $ assertEqual "lookMissing"
("Unknown Item", 0)
(look codeIndex 3815)
]
Don’t worry about the dollar signs just yet.
Equipped with those tests, we can now work through the look
function’s implementation. We can think of that again as a 2-step process:
We can use a where
clause to hold the resulting comprehension, like so:
look db code = if null results then unknownItem else results !! 0
where results = [(name, price) | (code2, name, price) <- db, code2 == code]
Here list !! index
returns the entry in the list at the given index (it’s loosely equivalent to the array indexing operator in other languages). The list comprehension looks through the list of code triples in search of one that matches the given code.
Now that we have all the tools for generating a bill from a till, we can focus on converting the bill to a string. We can break that up in steps as well:
The formatBill
method puts all those together:
formatBill :: Bill -> String
formatBill bill = formatLines bill ++ formatTotal total
where total = makeTotal bill
formatLines :: Bill -> String
formatLines bill = "" -- Need to fix this
formatTotal :: Price -> String
formatTotal total = "" -- Need to fix this
makeTotal :: Bill -> Price
makeTotal bill = 0
We can add some tests:
TestCase $ assertEqual "makeTotal"
(23 + 45)
(makeTotal [("something", 23), ("else", 45)]),
TestCase $ assertEqual "formatTotal"
"\nTotal.....................6.61"
(formatTotal 661),
TestCase $ assertEqual "formatBill"
("Dry Sherry, 1lt...........5.40\n" ++
"Fish Fingers..............1.21\n" ++
"\nTotal.....................6.61")
(formatBill [("Dry Sherry, 1lt", 540), ("Fish Fingers", 121)]),
Let’s start with makeTotal
. We can consider it as a composition of two steps:
We can combine the two steps as follows:
makeTotal :: Bill -> Price
makeTotal bill = sum prices
where prices = [p | (_, p) <- bill]
Next let’s format the total. The format we are after for all items is as follows:
We will write a function that accomplishes this, then return to writing the rest of the total format:
formatLine :: BillItem -> String
formatLine (name, price) = name ++ filler ++ formattedPrice
where formattedPrice = formatPrice price
space = lineLength - length name - length formattedPrice
filler = replicate space '.'
formatPrice :: Price -> String
formatPrice price = "" -- Need to fix this
And let’s add a test for it, as well as tests for the formatPrice
method:
TestCase $ assertEqual "normal amount" "12.53" (formatPrice 1253),
TestCase $ assertEqual "single digit pennies" "12.03" (formatPrice 1203),
TestCase $ assertEqual "no dollars" "0.53" (formatPrice 53),
TestCase $ assertEqual "whole line"
"Dry Sherry, 1lt...........5.40"
(formatLine ("Dry Sherry, 1lt", 540)),
Formatting the price consists of printing the dollar part (using show
), using a dot, then the pennies using two digits (so 09
instead of just 9
).
formatPrice :: Price -> String
formatPrice price = show dollars ++ "." ++ space ++ show pennies
where dollars = price `div` 100
pennies = price `mod` 100
space = if pennies < 10 then "0" else ""
TODO:
formatLines :: Bill -> String
formatLines lines = join "\n" [formatLine l | l <- lines]
where join sep lines = intercalate sep lines
formatTotal :: Price -> String
formatTotal total = "\n" ++ formatPrice ("Total", total)