2014-07-02 13 views
14

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.緯度,經度和高程在Haskell中應該有自己的類型嗎?


我開始學習Haskell的像一個星期前(從Python和C#來了),我想實現一個類GeographicPosition器,存儲緯度,經度和海拔。

具體而言,我希望以最優雅,功能強大的「測量單位」方式進行操作。

例如,如果我們將笛卡爾(「矩形」)空間中的X,Y和Z表示爲相同的東西,它們具有相同的範圍(從-inf+inf)是正交且均勻的。

現在與緯度,經度和海拔這不是那樣的。例如,經度是週期性的,緯度在極點上有一些最大範圍(它們本身就是奇點),並且海拔在地球中心有一個最小絕對值(另一個奇點)。

除了奇異點之外,從X,Y和Z在笛卡爾系統中是「同一事物」的角度來看,它們顯然(至少對我而言)它們不是「同一個事物」。我不能簡單地翻轉原點,並假裝緯度是現在的經度,以便我可以假裝X現在是Y,等等。

所以,問題是:

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)

我會沿着想象像

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

,而不是更加明顯,基於位置的

data Position = Position RealFloat RealFloat RealFloat 

但我不知道哪種風格更受建議。看起來好像Bounded也是一個有趣的構造,但我不太明白在這種情況下如何使用它。

+0

如有疑問,請換一種新型。在這種情況下,您可以使用'newtype'和智能構造函數。 – nomen

回答

13

我個人會爲他們製作一個類型,如果你真的想確保將事物保持在其週期性範圍內,那麼這是一個很好的機會來確保它。

開始通過製作簡單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 

(注:這是不是最有效的解決方案,但我想保持簡單用於說明目的)

現在你可以使用這些創建「智能構造函數」。這實質上就是Data.Ratio.Ratio類型與%函數的作用,以確保您提供參數Integral並減少該比例,並且不會導出實際的數據構造函數:%

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

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

這些是你從你的模塊出口,以確保沒有一個誤用LatitudeLongitude類型的功能。接下來,你可以寫實例像Num調用normLatnormLong內部:

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 

,類似的還有Longitude

然後,您可以安全地對LatitudeLongitude值執行算術運算,而不必擔心它們超出有效範圍,即使將它們饋送到其他庫的函數中也是如此。如果這看起來像樣板,那就是。可以說,有更好的方法來做到這一點,但是經過一些設置之後,你就有了一個難以打破的一致的API。


一個很好的特點實施Num類型類讓你對整數文字轉換成您的自定義類型的能力。如果您通過fromRational函數實現Fractional類,則會爲您的類型獲得完整的數字文字。假設你同時實施得當,你可以做這樣的事情

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

當然,你需要確保該normalize功能實際上是你要使用的一個,還有,你可以插上不同的實現那裏得到可用的值。你可能會決定你想要Latitude 1 + Latitude 90 == Latitude 89Latitude 1的實例(在達到上限後,數值「反彈」),或者你可以讓它們環繞到下限,以便Latitude 1 + Latitude 90 == Latitude -89,或者你可以保留它在這裏它只是增加或減少界限,直到它在範圍內。這取決於你的使用情況。

+0

那完全是我想要的那種答案。謝謝(即使我當然還是不瞭解所有事情,但現在我已經完成了我的作業:) – heltonbiker

+0

@heltonbiker那麼,你不明白什麼?也許我可以讓事情變得更加清晰。 – bheklilr

+0

'實例'部分似乎解決了我已經擁有的一個問題。我嘗試了你的代碼 'main = do print $ position {latitude = 0,longitude = 0,elevation = 0}' 並且得到一個錯誤: 「'沒有實例(Num Longitude) ' 可能的修正:爲(Num Longitude)添加一個實例聲明 在'經度'字段中的一個記錄'「 – heltonbiker

6

對每個字段使用不同類型的替代方法是使用封裝:爲您的職位創建一個抽象數據類型,使所有字段爲私有,並且只允許用戶使用您提供的公共接口與職位進行交互。

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並尊重位置不變量。

查看更多關於http://www.haskell.org/haskellwiki/Smart_constructors

+0

那麼,在我的例子中,我正在做所有在運行時檢查,沒有任何花哨的類型系統技巧。它只是普通的舊的抽象數據類型,如70年代所見:) – hugomg

+3

ADT方法非常棒,因爲它允許您從位置表示中抽象出來。緯度/經度/高程系統自然地將每個位置表示爲等價類而不是單個表示。你可以避免正常化,直到它真的適合你。例如,您可以執行整個序列的操作,例如「向北移動0.02,向東移動0.013,向下移動12742公里,向南移動0.5」而不進行標準化,並且只有在有人要求標準表示時才進行標準化。 – dfeuer

+0

實際上,如果其他方法對您的計算效果更好,您甚至不必使用緯度/經度/高程在內部進行存儲。 – dfeuer