对于对此主题感兴趣的人:接受的答案涉及一些我认为在这里有详细描述的概念.也就是说,之间的差异
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,或者你可以保留它我在这里的方式只需添加或减去边界,直到它在范围内.由您决定哪种实现适合您的用例.
为每个字段使用单独类型的替代方法是使用封装:为您的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