Latitude,Longitude和Elevation在Haskell中应该有自己的类型吗?

hel*_*ker 14 haskell types

对于对此主题感兴趣的人:接受的答案涉及一些我认为在这里有详细描述的概念.也就是说,之间的差异data,newtype以及instance关键字,并使用它们的方式.


我开始像一周前学习Haskell(来自Python和C#),我想实现一个GeographicPosition存储Latitude,Longitude和Elevation的类.

具体来说,我想用最优雅,功能性的"测量单位"识别方式来实现.

例如,如果我们在笛卡尔("矩形")空间中采用X,Y和Z,它们都意味着相同的东西,具有相同的范围(从-inf到+inf),正交和均匀.

现在有纬度,经度和海拔,这不是那样的.例如,经度是周期性的,纬度在极点处具有一些最大范围(它们本身是奇点),并且高度在地球中心具有最小绝对值(另一个奇点).

除了奇点之外,很明显(至少对我来说)它们不是"同一个东西",因为X,Y和Z在笛卡尔系统中是"相同的东西".我不能简单地翻转原点并假装Latitude现在是经度,我可以假装X现在是Y,等等.

所以问题是:

Latitude,Longitude和Elevation在Haskell中代表地理位置的类型中是否有自己的数字类型?什么是一个好的类型签名(最小的示例代码将是伟大的)

我会想象一下

data Position = Position { latitude :: Latitude,
                           longitude :: Longitude,
                           elevation :: Elevation }
Run Code Online (Sandbox Code Playgroud)

而不是更明显的,基于位置的

data Position = Position RealFloat RealFloat RealFloat 
Run Code Online (Sandbox Code Playgroud)

但我不知道哪种风格更值得推荐.它似乎Bounded也是一个有趣的结构,但我不太明白如何在这种情况下使用它.

bhe*_*ilr 13

我个人会为他们做一个类型,如果你真的想确保将事情保持在他们的周期范围内,那么这是一个确保这一点的好机会.

首先制作简单的newtype构造函数:

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

newtype Longitude = Longitude Double deriving (Eq, Show, Ord)
Run Code Online (Sandbox Code Playgroud)

请注意,我没有使用RealFloat,因为RealFloat是类型类,而不是具体类型,所以它不能用作构造函数的字段.接下来,编写一个函数来规范化这些值:

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
Run Code Online (Sandbox Code Playgroud)

(注意:这不是最有效的解决方案,但我希望将其简化为说明目的)

现在您可以使用它们来创建"智能构造函数".这实际上是Data.Ratio.Ratio类型对%函数的作用,以确保您提供Integral参数并减少该分数,并且它不会导出实际的数据构造函数:%.

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

mkLong :: Double -> Longitude
mkLong = normLong . Longitude
Run Code Online (Sandbox Code Playgroud)

这些是您从模块中导出的函数,以确保没有人滥用Latitude和Longitude类型.接下来,你可以写这样的实例Num是呼叫normLat和normLong内部:

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
Run Code Online (Sandbox Code Playgroud)

同样的Longitude.

然后,您可以安全地执行算术Latitude和Longitude值,而不必担心它们会超出有效范围,即使您将它们输入到其他库中的函数中也是如此.如果这看起来像样板,那就是.可以说,有更好的方法可以做到这一点,但经过一些设置后,您就拥有了一个难以打破的一致API.


实现Num类型类的一个非常好的功能使您能够将整数文字转换为自定义类型.如果您Fractional使用其fromRational功能实现该类,您将获得类型的完整数字文字.假设你已经正确实施,你可以做一些事情

> 1 :: Latitude
Latitude 1.0
> 91 :: Latitude
Latitude 1.0
> 1234.5 :: Latitude
Latitude 64.5
Run Code Online (Sandbox Code Playgroud)

当然,您需要确保该normalize功能实际上是您要使用的功能,您可以插入不同的实现以获取可用的值.你可能会决定你想要的Latitude 1 + Latitude 90 == Latitude 89实例Latitude 1(在到达上限后值"反弹"),或者你可以将它们环绕到下限,这样Latitude 1 + Latitude 90 == Latitude -89,或者你可以保留它我在这里的方式只需添加或减去边界,直到它在范围内.由您决定哪种实现适合您的用例.


hug*_*omg 6

为每个字段使用单独类型的替代方法是使用封装:为您的Position创建一个抽象数据类型,使所有字段都是私有的,并且只允许用户使用您提供的公共接口与位置进行交互.

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.
Run Code Online (Sandbox Code Playgroud)

这样做的主要优点是类型系统样板更少,并且您使用的类型更简单.只要您的库足够小,您就可以将所有函数保留在头脑中,命名约定+测试应该足以使您的函数无bug且尊重Position不变量.

有关详情,请访问http://www.haskell.org/haskellwiki/Smart_constructors

  • ADT方法很棒,因为它允许您从位置表示中抽象出来.纬度/经度/高度系统自然地将每个位置表示为等价类而不是单个表示.你可以避免正常化,直到它真正适合你.例如,您可以执行一系列操作,例如"向北移动0.02,向东移动0.013,向下移动12742 km,向南移动0.5"而不进行标准化,并且仅在有人要求标准表示时进行标准化. (3认同)