Search code examples
haskellghciwinghci

How can I compare and return data using a list of data


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.


Solution

  • 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.)