In this assignment we get a bit more practice breaking down a problem into smaller parts and using the various Haskell facilities to do the appropriate transformations.
The problem we will solve is a voting system called Borda count. The situation is as follows:
We will write the code that does this. For example our code will be given an input such as:
allVotes = [
["Peter", "Debra", "Oliver", "John"],
["Peter", "Oliver", "John", "Debra"],
["Oliver", "Debra", "John", "Peter"],
["John", "Oliver", "Debra", "Peter"],
["Debra", "John", "Oliver", "Peter"]]
getResults allVotes
-- Will produce the string:
-- Oliver 14
-- Debra 13
-- John 12
-- Peter 11
Quick note: To print a string on the interactive console, use putStr
or putStrLn
, for example:
putStrLn "Hello there!"
Use this only for diagnostic purposes. Your functions should simply be returning strings and not printing directly.
Here is some start code. You may want to add your own tests.
module Voting where
import Test.HUnit
import Data.List (sortBy, nub, sort)
type Candidate = String
type Vote = [Candidate]
allVotes :: [Vote]
allVotes = [
["Peter", "Debra", "Oliver", "John"],
["Peter", "Oliver", "John", "Debra"],
["Oliver", "Debra", "John", "Peter"],
["John", "Oliver", "Debra", "Peter"],
["Debra", "John", "Oliver", "Peter"]]
-- Function provided for you
getResults :: [Vote] -> IO ()
getResults votes = sequence_ [putStrLn s | s <- formatVotes votes]
where formatVotes = formatAll . sortedCounts . totalCounts
tests = TestList [
TestCase $ assertEqual "downToN" [5, 4, 3, 2, 1] (downFromN 5),
TestCase $ assertEqual "withRanks"
[("A", 3), ("B", 2), ("C", 1)]
(withRanks ["A", "B", "C"]),
TestCase $ assertEqual "allRanks"
(sort [("A", 3), ("B", 2), ("C", 1), ("B", 3), ("A", 2), ("C", 1)])
(sort (allRanks [["A", "B", "C"], ["B", "A", "C"]])),
TestCase $ assertEqual "totalRank"
5
(totalRank "B" [("B", 2), ("C", 1), ("B", 3)]),
TestCase $ assertEqual "allCandidates"
["A", "B"]
(sort (allCandidates [("B", 2), ("B", 3), ("A", 1), ("A", 1)])),
TestCase $ assertEqual "totalCounts"
[("A", 5), ("B", 3), ("C", 4)]
(sort (totalCounts [["A", "B", "C"], ["C", "A", "B"]])),
TestCase $ assertEqual "cmp1" EQ (cmp ("A", 2) ("B", 2)),
TestCase $ assertEqual "cmp2" LT (cmp ("A", 2) ("B", 1)),
TestCase $ assertEqual "cmp3" GT (cmp ("A", 2) ("B", 3)),
TestCase $ assertEqual "sortedCounts"
[("A", 5), ("C", 4), ("B", 3)]
(sortedCounts [("A", 5), ("B", 3), ("C", 4)]),
TestCase $ assertEqual "neededLength" 5 (neededLength ("Jo", 23)),
TestCase $ assertEqual "totalNeededLength"
9
(totalNeededLength [("Jo", 23), ("Patrick", 4), ("Peter", 123)]),
TestCase $ assertEqual "formatPair"
"Jo 123"
(formatPair 9 ("Jo", 123)),
TestCase $ assertEqual "formatAll"
["Jo 123", "Peter 23"]
(formatAll [("Jo", 123), ("Peter", 23)])
]
Remember to run your tests with:
runTestTT tests
Note that there are two type aliases, one to represent candidates as strings and another to represent a “Vote” as a list of candidates.
Here are the functions you should implement. You should start by specifying their types and a “stub” implementation that does nothing. That way your tests will be runnable.
downFromN
. It takes as input an integer n
and returns the list of numbers from n
down to 1
.withRanks
. It takes a vote and returns a list of candidate-integer pairs where the ranks have been added. For example the vote ["A", "B","C"]
becomes [("A", 3), ("B", 2), ("C", 1)]
. You can achieve this easily using zip
.allRanks
. It takes a list of votes and returns a list of all the pairs produced by withRanks
for each of the votes in the list. So this will merge all the votes together into one big list, which is OK since we know how many points each candidate got from a voter by the rank number. For example the two votes [["A", "B", "C"], ["B", "A", "C"]]
would produce (in some order) the list [("A", 3), ("B", 2), ("C", 1), ("B", 3), ("A", 2), ("C", 1)]
.totalRank
. This takes in a candidate and a list of candidate-integer pairs, and adds up the integers only for those pairs matching the candidate. For example for candidate "B"
and ranked pairs [("B", 2), ("C", 1), ("B", 3)]
the answer would be 5
.allCandidates
which takes a list of candidate-integers pairs and returns a list of the candidates only, removing duplicates. The function nub
will help you remove duplicates.totalCounts
which takes a list of votes and returns a list of candidate-integer pairs which lists each candidate once, with corresponding number being the number of points they got. See the example in the test. The order of the candidates does not matter at this point.cmp
function that takes in two “candidate-integer” pairs and “compares” them by simply comparing the integers using the built-in compare
function, and in reverse order (so a pair is “smaller” than another pair if its integer is larger). This will list larger integers first. The return type of this function if a bit unusual: it’s a type called Ordering
with the three values LT
for “less than”, EQ
for “equal” and GT
for “greater than”. You don’t need to return these values directly, the call you make to the integer compare
function does that.sortedCounts
function. It takes as input the list of candidate-integer pairs, and returns the “sorted” list using the sort based on the cmp
function. You can achieve this by calling the sortBy
function and giving it the cmp
function as its first parameter (and your list as its second).neededLength
that returns, for a candidate-integer pair, the length needed to properly present it, which should be the length of the candidate followed by one space followed by the length of the integer. You can use the show
function to turn the integer into a string. Look at the test for example input.totalNeededLength
. This takes a list of candidate-integer pairs, and produces the line length needed to be able to show all them (so it should be the largest of their individual lengths). You will need a list comprehension together with the list maximum
, which returns the largest number from a list.formatPair
function that takes an integer length and a candidate-integer pair and returns a string of that length that starts with the candidate, ends with the integer, and fills the remaining space with empty spaces.formatAll
which takes a list of candidate-integer pairs and returns a list of strings, where each pair has been formatted using formatPair
, and where the length used is the total needed length produced by totalNeededLength
.getResults
puts all these together and is provided for you. You can run it as:getResults allVotes