Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should Latitude, Longitude and Elevation have their own type in Haskell?

Tags:

types

haskell

For people interested in this topic: the accepted answer involves some concepts that I think are well described here. Namely, differences between data, newtype and instance keywords, and ways to use them.


I started to learn Haskell like a week ago (coming from Python and C#), and I want to implement a class GeographicPosition, that stores Latitude, Longitude and Elevation.

Specifically, I'd like to do it in the most elegant, functional, "unit-of-measurement" aware way.

If we take, for example, X, Y and Z in cartesian ("rectangular") space, they all mean the same thing, have the same range (from -inf to +inf), being orthogonal and uniform.

Now with Latitude, Longitude and Elevation this is not like that. Longitude, for example, is periodic, Latitude has some maximum rangess at the poles (which are themselves singularities), and the elevation has a minimum absolute value at the center of the earth (another singularity).

Singularities apart, it is obvious (to me at least) that they are not "the same thing", in the sense that X, Y and Z are "the same thing" in a cartesian system. I cannot simply flip the origin and pretend that Latitude is now Longitude in the way that I can pretend that X now is Y, and such.

So the question is:

Should Latitude, Longitude and Elevation have their own numerical type in a type representing geographical position in Haskell? What would be a good type signature for that (a minimal sample code would be great)

I would imagine something along like

data Position = Position { latitude :: Latitude,
                           longitude :: Longitude,
                           elevation :: Elevation }

instead of the more obvious, position-based

data Position = Position RealFloat RealFloat RealFloat 

but I don't know which style is more advised. It seems like Bounded is an interesting construct too, but I didn't quite understood how to use it in this case.

like image 212
heltonbiker Avatar asked Jul 02 '14 02:07

heltonbiker


2 Answers

I personally would make a type for them, and if you really want to make sure to keep things within their periodic bounds then this is a great opportunity to ensure that.

Start by making simple newtype constructors:

newtype Latitude = Latitude Double deriving (Eq, Show, Ord)

newtype Longitude = Longitude Double deriving (Eq, Show, Ord)

Note that I didn't use RealFloat, because RealFloat is a typeclass, not a concrete type so it can't be used as a field to a constructor. Next, write a function to normalize these values:

normalize :: Double -> Double -> Double
normalize upperBound x
    | x >  upperBound = normalize upperBound $ x - upperBound
    | x < -upperBound = normalize upperBound $ x + upperBound
    | otherwise       = x

normLat :: Latitude -> Latitude
normLat (Latitude x) = Latitude $ normalize 90 x

normLong :: Longitude -> Longitude
normLong (Longitude x) = Longitude $ normalize 180 x

(Note: This is not the most efficient solution, but I wanted to keep it simple for illustrative purposes)

And now you can use these to create "smart constructors". This is essentially what the Data.Ratio.Ratio type does with the % function to ensure that you provide Integral arguments and reduce that fraction, and it doesn't export the actual data constructor :%.

mkLat :: Double -> Latitude
mkLat = normLat . Latitude

mkLong :: Double -> Longitude
mkLong = normLong . Longitude

These are the functions that you'd export from your module to ensure that no one misuses the Latitude and Longitude types. Next, you can write instances like Num that call normLat and normLong internally:

instance Num Latitude where
    (Latitude x) + (Latitude y) = mkLat $ x + y
    (Latitude x) - (Latitude y) = mkLat $ x - y
    (Latitude x) * (Latitude y) = mkLat $ x * y
    negate (Latitude x) = Latitude $ negate x
    abs (Latitude x) = Latitude $ abs x
    signum (Latitude x) = Latitude $ signum x
    fromInteger = mkLat . fromInteger

And similarly for Longitude.

You can then safely perform arithmetic on Latitude and Longitude values without worrying about them ever going outside valid bounds, even if you're feeding them into functions from other libraries. If this seems like boilerplate, it is. Arguably, there are better ways to do this, but after a little bit of set up you have a consistent API that is harder to break.


One really nice feature implementing the Num typeclass gives you is the ability to convert integer literals to your custom types. If you implement the Fractional class with its fromRational function, you'll get full numeric literals for your types. Assuming you have both implemented properly, you can do things like

> 1 :: Latitude
Latitude 1.0
> 91 :: Latitude
Latitude 1.0
> 1234.5 :: Latitude
Latitude 64.5

Granted, you'll need to make sure that the normalize function is actually the one you want to use, there are different implementations that you could plug in there to get usable values. You might decide that you want Latitude 1 + Latitude 90 == Latitude 89 instance of Latitude 1 (where the values "bounce" back after the reach the upper bound), or you can have them wrap around to the lower bound so that Latitude 1 + Latitude 90 == Latitude -89, or you can keep it how I have it here where it just adds or subtracts the bound until it's within range. It's up to you which implementation is right for your use case.

like image 141
bheklilr Avatar answered Sep 17 '22 12:09

bheklilr


An alternative to using separate types for every field is to use encapsulation: create an abstract data type for your Positions, make all the fields private and only let users interact with positions using the public interface you provide.

module Position (
    Position, --export position type but not its constructor and field accessor.
    mkPosition, -- smart constructor for creating Positions
    foo -- your other public functions
) where 

-- Named fields and named conventions should be enough to
-- keep my code sane inside the module
data Position = Position {
  latitude :: Double,
  longitude :: Double,
  elevation :: Double
} deriving (Eq, Show)

mkPosition :: Double -> Double -> Double -> Position
mkPosition lat long elev =
   -- You can use this function to check invariants
   -- and guarantee only valid Positions are created.

The main advantage of doing this is that there is less type system boilerplate and the types you work with are simpler. As long as your library is small enough, you can keep all the functions in your head the naming conventions + testing should be enough to make your functions bug free and respecting the Position invariants.

See more on http://www.haskell.org/haskellwiki/Smart_constructors

like image 39
hugomg Avatar answered Sep 19 '22 12:09

hugomg