Haskell 在哈斯克尔,纬度、经度和海拔应该有自己的类型吗?

Haskell 在哈斯克尔,纬度、经度和海拔应该有自己的类型吗?,haskell,types,Haskell,Types,对于对这个话题感兴趣的人:公认的答案包括一些我认为描述得很好的概念。也就是说,数据、新类型和实例关键字之间的差异,以及使用它们的方法 一周前我开始学习Haskell(来自Python和C#),我想实现一个类GeographicPosition,它存储纬度、经度和海拔 具体地说,我想用最优雅、最实用的“度量单位”感知方式来做 例如,如果我们以笛卡尔(“矩形”)空间中的X、Y和Z为例,它们都意味着相同的东西,具有相同的范围(从-inf到+inf),是正交且一致的 现在有了纬度,经度和海拔,情况就不

对于对这个话题感兴趣的人:公认的答案包括一些我认为描述得很好的概念。也就是说,
数据
新类型
实例
关键字之间的差异,以及使用它们的方法


一周前我开始学习Haskell(来自Python和C#),我想实现一个类
GeographicPosition
,它存储纬度、经度和海拔

具体地说,我想用最优雅、最实用的“度量单位”感知方式来做

例如,如果我们以笛卡尔(“矩形”)空间中的X、Y和Z为例,它们都意味着相同的东西,具有相同的范围(从
-inf
+inf
),是正交且一致的

现在有了纬度,经度和海拔,情况就不同了。例如,经度是周期性的,纬度在两极有一些最大范围(它们本身就是奇点),海拔在地球中心有一个最小绝对值(另一个奇点)

撇开奇点不谈,很明显(至少对我来说)它们不是“同一件事”,在这个意义上,X、Y和Z在笛卡尔系统中是“同一件事”。我不能简单地翻转原点,假设纬度现在是经度,就像我可以假设X现在是Y一样

因此,问题是:

纬度、经度和海拔是否应该在代表哈斯克尔地理位置的类型中有自己的数字类型?什么样的类型签名比较好(一个最小的示例代码就好了)

我会想象这样的事情

data Position = Position { latitude :: Latitude,
                           longitude :: Longitude,
                           elevation :: Elevation }
而不是更明显的,基于位置的

data Position = Position RealFloat RealFloat RealFloat 

但我不知道哪种风格更可取。似乎
Bounded
也是一个有趣的构造,但我不太明白在这种情况下如何使用它。

我个人会为它们制作一个类型,如果你真的想确保事物保持在它们的周期性范围内,那么这是一个很好的机会来确保这一点

首先创建简单的
newtype
构造函数:

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

newtype Longitude = Longitude Double deriving (Eq, Show, Ord)
请注意,我没有使用
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
您可以从模块中导出这些函数,以确保没有人误用
纬度
经度
类型。接下来,您可以编写像
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
同样地,对于
经度

然后,您可以安全地对
纬度
经度
值执行算术运算,而不必担心它们超出有效边界,即使您正在将它们从其他库馈送到函数中。如果这看起来像样板,那就是。可以说,有更好的方法可以做到这一点,但经过一点设置后,您就有了一个更难打破的一致API


实现
Num
typeclass的一个非常好的特性是能够将整数文本转换为自定义类型。如果您使用它的
fromRational
函数实现
fractal
类,您将获得类型的完整数字文本。假设两者都正确实现了,您可以执行以下操作

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

当然,您需要确保
normalize
函数实际上是您想要使用的函数,您可以插入不同的实现以获得可用的值。您可以决定想要
纬度1+纬度90==纬度89
纬度1
实例(其中值在到达上限后“反弹”回来),或者您可以让它们环绕到下限,以便
纬度1+纬度90==纬度-89
,或者你可以把它保留在这里,它只是加上或减去边界,直到它在范围内。由您决定哪个实现适合您的用例。

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

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.
这样做的主要优点是,类型系统样板文件更少,使用的类型更简单。只要你的库足够小,你就可以把所有函数都放在脑子里——命名约定+测试应该足以让你的函数摆脱bug,并尊重位置不变量


更多信息请参见

,这正是我想要的答案。谢谢你(虽然我当然还不懂所有的东西,但现在我有作业了:)@heltonbiker好吧,你还不懂什么?也许我可以帮你把事情弄清楚一点。
实例
部分似乎解决了我已经遇到的一个问题。我用
main=do print$Position{latitude=0,longitude=0,elevation=0}
尝试了您的代码,但出现了一个错误:
没有(Num-longitude)的实例,该实例是由文字“0”可能的修复引起的:在记录的“longitude”字段中添加(Num-longitude)的实例声明@heltonbiker您还必须为
经度
填写一个类似的
Num
实例,您只需复制/粘贴
纬度
的实例,并将所有
纬度
更改为
经度
,将
mkLat
更改为
mkLong
。我没有完全给你所有的=p
normLat
normLong
的意义是什么?如果只有两个操作可以生成
纬度
经度
mkLat
mkLong
,则将其标准化,所有操作都使用
mkLat
mkLongmodule 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.