patternMinor
Recommend Next Note Purchase
Viewed 0 times
recommendnotepurchasenext
Problem
I have just started trying to learn Haskell and tried to think of a practical, but simple problem to try and solve. I have a strong background in C, but am just beginning to dip my toes in the world of functional programming.
The idea came from my investing in notes on Prosper: given a list of note ratings, and a desired distribution, what should the next note purchase be, in order to most align with the desired distribution.
Here's an example:
Say I currently own 5 notes of an "A" rating, and 4 notes of a "B" rating. If my desired distribution is 50% "A" and 50% "B", then the next note purchased should be of a "B" rating. However, if my desired distribution was 90/10, the next note should be an "A" rating.
The below code seems to work (albeit with no error checking), but feels very unreadable. What are some ways I could refactor to make the code more readable or idiomatic to Haskell?
The idea came from my investing in notes on Prosper: given a list of note ratings, and a desired distribution, what should the next note purchase be, in order to most align with the desired distribution.
Here's an example:
Say I currently own 5 notes of an "A" rating, and 4 notes of a "B" rating. If my desired distribution is 50% "A" and 50% "B", then the next note purchased should be of a "B" rating. However, if my desired distribution was 90/10, the next note should be an "A" rating.
The below code seems to work (albeit with no error checking), but feels very unreadable. What are some ways I could refactor to make the code more readable or idiomatic to Haskell?
module Main where
import Data.List
import Data.Ord
main::IO()
main = print $ recommendNote [ "B", "C", "D", "D", "HR" ] [ 0, 0, 0, 0.5, 0.25, 0.25, 0]
noteTypes :: [String]
noteTypes = [ "AA", "A", "B", "C", "D", "E", "HR" ]
recommendNote :: (Ord a, Fractional a) => [String] -> [a] -> String
recommendNote notes targetDist = let y = zip (subtractLists targetDist (getDistribution notes)) noteTypes
in snd (maximumBy (comparing fst) y)
subtractLists :: (Num a) => [a] -> [a] -> [a]
subtractLists = zipWith (-)
count :: [String] -> String -> Int
count inList x = length $ filter (x==) inList
getNoteCount :: [String] -> [Int]
getNoteCount inList = map (count inList) noteTypes
getDistribution :: (Fractional a) => [String] -> [a]
getDistribution inList = percentize $ map fromIntegral (getNoteCount inList)
percentize :: (Fractional a) => [a] -> [a]
percentize inList = map (/ sum inList) inListSolution
Order of arguments
As a convention, arguments in functions are always ordered from most likely to change to less likely to change.
Because this allows...
Simplification (Currying)
Haskell is like Math.
Look at
You would like the simplify the
The same applies to your code:
You can omit
Becomes:
Now the focus is not on the transformation of the argument, but on the definition of a function as the successive application of two (or more) known ones.
Another example is:
That becomes:
This is, left to right, a description of what the function does.
Conventional names
I see that
Just use the conventional
Use
It is more natural to read from general to specific than the opposite:
Becomes:
Where you also avoid overly long lines.
Generality
Be optimistic and general with your types:
Becomes:
Where
Not everything is
Make tiny functions local
Things like:
May be local and without a type declaration, like:
As a convention, arguments in functions are always ordered from most likely to change to less likely to change.
Because this allows...
Simplification (Currying)
Haskell is like Math.
Look at
2x + x = 1 + xYou would like the simplify the
x, so:2x = 1The same applies to your code:
percentize inList = map (/ sum inList) inListYou can omit
inList as it is just repeated on both sides:count x inList = length $ filter (x==) inListBecomes:
count x = length . filter (x==)Now the focus is not on the transformation of the argument, but on the definition of a function as the successive application of two (or more) known ones.
Another example is:
getDistribution inList = percentize $ map fromIntegral (getNoteCount inList)That becomes:
getDistribution = percentize . map fromIntegral . getNoteCountThis is, left to right, a description of what the function does.
Conventional names
I see that
inList goes inside the function, and I can see that it is a List from the type signature, such long names are not needed.Just use the conventional
xs for general lists: percentize xs = map (/ sum xs) xsUse
where not letIt is more natural to read from general to specific than the opposite:
recommendNote notes targetDist = let y = zip (subtractLists targetDist (getDistribution notes)) noteTypes
in snd (maximumBy (comparing fst) y)Becomes:
recommendNote notes targetDist = snd (maximumBy (comparing fst) y)
where
y = zip (subtractLists targetDist (getDistribution notes)) noteTypesWhere you also avoid overly long lines.
Generality
Be optimistic and general with your types:
count :: String -> [String] -> IntBecomes:
count :: a -> [a] -> IntWhere
a stands for anything.Not everything is
getget is a specific OO technical term, prefixing it before the function names is confusing, just drop it.Make tiny functions local
Things like:
subtractLists = zipWith (-)May be local and without a type declaration, like:
recommendNote notes targetDist = snd (maximumBy (comparing fst) y)
where
y = zip (subtractLists targetDist (getDistribution notes)) noteTypes
subtractLists = zipWith (-)Code Snippets
2x + x = 1 + xpercentize inList = map (/ sum inList) inListcount x inList = length $ filter (x==) inListcount x = length . filter (x==)getDistribution inList = percentize $ map fromIntegral (getNoteCount inList)Context
StackExchange Code Review Q#113989, answer score: 3
Revisions (0)
No revisions yet.