Problem
The task in question allows a user to re-rate a film such that only the latest rating from the user should remain in the database.
Initially I solved this problem in a very basic manor?
This was my result:
type Rating = (String, Int)
type Film = (String, String, Int, [Rating])
--Example ("Blade Runner","Ridley Scott",1982, [("Amy",5),("Bill",8)])
where the functions:
--Find selected film, isolate into new List
getFilm :: String -> [Film] -> [(Film)]
getFilm search aFilm = [(title,dir,year,ratings) | (title,dir,year,ratings) <- aFilm, search == title]
--Update the new film and pull details for amendment, STORE
pullTitle :: [(Film)] -> String
pullTitle [(title,_,_,_)] = title
pullDirector :: [(Film)] -> String
pullDirector [(_,director,_,_)] = director
pullYear :: [(Film)] -> Int
pullYear [(_,_,year,_)] = year
pullRatings :: [(Film)] -> [Rating]
pullRatings [(_,_,_,rating)] = rating
newRatings :: [Rating] -> Rating -> [Rating]
newRatings oldRatings (newUser,newRate) = [(user,rate) | (user,rate) <- oldRatings, user /= newUser] ++ [(newUser,newRate)]
--Database without selected film
updateDatabase :: String -> [Film] -> [(Film)]
updateDatabase search aFilm = [(title,dir,year,ratings) | (title,dir,year,ratings) <- aFilm, search /= title]
--Adds new film to a database
updatedFilm :: String -> String -> Int -> [Rating] -> [Film] -> [Film]
updatedFilm name director yearOfRel ratings database = database ++ [(name,director,yearOfRel,ratings)]
--Add new film to updated Database
formulateFilm :: [(Film)] -> [(Film)] -> Rating -> [(Film)]
formulateFilm soloFilm updatedFilms updateRating = updatedFilm (pullTitle soloFilm) (pullDirector soloFilm) (pullYear soloFilm) (newRatings (pullRatings soloFilm) updateRating) updatedFilms
--Compiling to for finalcome
rateMovie :: String -> Rating -> [Film] -> [(Film)]
rateMovie mTitle newRating oldDatabase = formulateFilm (getFilm mTitle oldDatabase) (updateDatabase mTitle oldDatabase) newRating
I have changed the following functions to make use of filters:
getFilm :: String -> [Film]
getFilm search = filter((title,_,_,_) -> search == title) testDatabase
updateDatabase :: String -> [Film]
updateDatabase search = filter((title,_,_,_) -> search /= title) testDatabase
Could someone advise me on how I would use maps to get the selected films data from getFilm :: String -> [Film]
, similar to the (pull*) functions?
Solution
It is better to use records instead of tuples here:
data Rating = Rating
{ user :: String
, rate :: Int
} deriving Show
data Film = Film
{ title :: String
, director :: String
, year :: Int
, ratings :: [Rating]
} deriving Show
Thus you get self documenting code and all of pull*
functions for free (for each field compiler generates accessor function with the same name).
This also leads to more concise implementation of other functions:
-- Find films by name
getFilm :: String -> [Film] -> [Film]
getFilm search = filter ((== search) . title)
Note that in Haskell there is no single element tuple, so parentheses around Film
are redundant ([Film]
but not [(Film)]
).
newRating :: Rating -> [Rating] -> [Rating]
newRating r = (r :) . filter ((/=user r) . user)
I switched arguments in newRating
because this allows to define function more concisely and it is also correlates with update/insert from Data.Map
: they have collection as a last argument.
Now it is possible to skip directly to rateMovie
and implement it in couple of lines:
rateMovie :: String -> Rating -> [Film] -> [Film]
rateMovie mTitle nRating database
= [m {ratings = newRating nRating (ratings m)} | m <- matchingMovies]
++ database'
where
(matchingMovies, database') = partition ((==mTitle).title) database
(Btw, why “movie”? we were talking about “films” all the way.)
I’m using partition from Data.List
to extract matching movie(s) from database.
Also there is funny syntax I’m using to update movies:
m {ratings = ...}
You can read more about it here.
And of course it is better to use Data.Map
instead of simple lists if the code is full of lookups.