I'm a newbie to Haskell and I'm struggling to find a way to use class member variables to return the member variable I am looking for. I have this data:
data Place = Place {name :: String,
north :: Float,
east :: Float,
rainfall :: [Int]
} deriving (Eq, Ord, Show)
testData :: [Place]
testData = [
Place "London" 51.5 (-0.1) [0, 0, 5, 8, 8, 0, 0],
Place "Norwich" 52.6 (1.3) [0, 6, 5, 0, 0, 0, 3],
Place "Birmingham" 52.5 (-1.9) [0, 2, 10, 7, 8, 2, 2],
Place "Hull" 53.8 (-0.3) [0, 6, 5, 0, 0, 0, 4],
Place "Newcastle" 55.0 (-1.6) [0, 0, 8, 3, 6, 7, 5],
Place "Aberdeen" 57.1 (-2.1) [0, 0, 6, 5, 8, 2, 0],
Place "St Helier" 49.2 (-2.1) [0, 0, 0, 0, 6, 10, 0]
]
What I'm trying to do is to return a place closest to a given location. So far I am able to calculate the distances for each place to the given location, and I know exactly which Item should be returned, but I don't know how to actually go about doing this. This is the code I have so far;
closestDry :: Float -> Float -> [Place] -> [Float]
closestDry _ _ [] = []
closestDry lx ly (x:xs) = distance(lx)(ly)(north x)(east x)):closestDry lx ly xs
distance :: Float -> Float -> Float -> Float -> Float
distance x1 y1 x2 y2 = sqrt ((y1 - y2)^2 + (x1 - x2)^2)
Typing into the console 'closestDry 51.5 (-0.1) testData' outputs:
[0.0,1.7804484,2.059126,2.3086786,3.8078866,5.946426,3.0479496]
I can see that the closest area must be "London" in order with the given list of places as the distance is '0.0', but how do I get this single Place returned to me?
I don't want to return the list of distances, but I can't figure out how to tell the function to get the smallest distance and return that corresponding Place, since it needs to be compared to the other places.
closestDry
is a basically-useless mess, so get rid of it. Then, let's write a distanceTo
function that gives you the distance from coordinates to a place:
distanceTo :: Float -> Float -> Place -> Float
distanceTo lat lon place = distance lat lon (north place) (east place)
Now, let's write a function that pairs the places with the distances to them:
distancesTo :: Float -> Float -> [Place] -> [(Place, Float)]
distancesTo lat lon = map (\place -> (place, distanceTo lat lon place))
Trying it out:
λ> distancesTo 51.5 (-0.1) testData
[(Place {name = "London", north = 51.5, east = -0.1, rainfall = [0,0,5,8,8,0,0]},0.0),(Place {name = "Norwich", north = 52.6, east = 1.3, rainfall = [0,6,5,0,0,0,3]},1.7804484),(Place {name = "Birmingham", north = 52.5, east = -1.9, rainfall = [0,2,10,7,8,2,2]},2.059126),(Place {name = "Hull", north = 53.8, east = -0.3, rainfall = [0,6,5,0,0,0,4]},2.3086786),(Place {name = "Newcastle", north = 55.0, east = -1.6, rainfall = [0,0,8,3,6,7,5]},3.8078866),(Place {name = "Aberdeen", north = 57.1, east = -2.1, rainfall = [0,0,6,5,8,2,0]},5.946426),(Place {name = "St Helier", north = 49.2, east = -2.1, rainfall = [0,0,0,0,6,10,0]},3.0479496)]
Looks right so far!
Now we can use minimumBy
, comparing
, and snd
to get the tuple, and then extract just the place with fst
:
import Data.Foldable (minimumBy)
import Data.Ord (comparing)
closestTo :: Float -> Float -> [Place] -> Place
closestTo lat lon places = fst $ minimumBy (comparing snd) (distancesTo lat lon places)
Let's try it:
λ> closestTo 51.5 (-0.1) testData
Place {name = "London", north = 51.5, east = -0.1, rainfall = [0,0,5,8,8,0,0]}
Success!
As an alternative to having distancesTo
, you could also calculate the distances with comparing
, like this:
closestTo :: Float -> Float -> [Place] -> Place
closestTo lat lon places = minimumBy (comparing (distanceTo lat lon)) places
This has the advantage of not needing any of the tuples, but the disadvantage of recomputing the distance for the same place multiple times.
Caveat to either way: minimumBy
is a dangerous partial function, which will crash your program if it ever gets an empty list, which will happen if closestTo
gets an empty list:
λ> closestTo 51.5 (-0.1) []
*** Exception: Prelude.foldl1: empty list
If you care about that, you'd need to avoid it by returning a Maybe Place
instead, and adjusting the code to return Nothing
when the input list is empty, instead of calling minimumBy
. (IMO, this is a wart in Haskell, and minimumBy
should just return a Maybe
itself instead of having to crash.)