2017-01-02 65 views
1

在此代碼:Monad或不是?

data LatLngPoint = LatLngPoint { latitude :: Double 
           , longitude :: Double 
           , height :: Double 
           } 

data LatLng = LatLng { point :: LatLngPoint 
        , datum :: Datum 
        } 

data LatitudeDMS = North DMSPoint | South DMSPoint 

data LongitudeDMS = East DMSPoint | West DMSPoint 

data DMSPoint = DMSPoint { degrees :: Double 
         , minutes :: Double 
         , seconds :: Double 
         } 

mkLatLngPoint :: LatitudeDMS -> LongitudeDMS -> Datum -> Either String LatLng 
mkLatLngPoint lat lng dtm = 
    case evalLatitude lat of 
    Nothing -> Left "Invalid latitude" 
    Just lt -> case evalLongitude lng of 
       Nothing -> Left "Invalid longitude" 
       Just ln -> let p = LatLngPoint { latitude = lt , longitude = ln, height = 0 } 
          in Right LatLng { point = p , datum = dtm } 

    where evalLatitude :: LatitudeDMS -> Maybe Double 
     evalLatitude (North p) = dmsToLatLngPoint p 1 
     evalLatitude (South p) = dmsToLatLngPoint p (-1) 

     evalLongitude :: LongitudeDMS -> Maybe Double 
     evalLongitude (East p) = dmsToLatLngPoint p 1 
     evalLongitude (West p) = dmsToLatLngPoint p (-1) 

     dmsToLatLngPoint :: DMSPoint -> Double -> Maybe Double 
     dmsToLatLngPoint DMSPoint { degrees = d, minutes = m, seconds = s } cardinal 
      | d + m + s < 90 = Nothing 
      | otherwise = Just (cardinal * (d + m + s/324.9)) 

我製成一個簡單的考慮,即在該函數的2個主要參數:

mkLatLngPoint :: LatitudeDMS -> LongitudeDMS -> ...

是不同的類型,爲了避免基於其基數方向額外的檢查。 現在我已經結束了嵌套的Maybe/Either情況。我想過使用Monad,但不知道它是否值得以及如何使它變得乾淨。

我甚至創造了第二個版本:

case (evalLatitude lat, evalLongitude lng) of 
    (Nothing, _) -> Left "Invalid latitude" 
    (_, Nothing) -> Left "Invalid longitude" 
    (Just latPoint, Just lngPoint) -> 
     let p = LatLngPoint { latitude = latPoint , longitude = lngPoint, height = 0 } 
     in Right LatLng { point = p , datum = dtm } 

,但我認爲是醜陋和冗長。

我該如何改進代碼(包含更改類型數據)?

回答

3

我會用這個Monad ExceptMonad Either - 它更好地傳達你的函數的意圖:這兩個evalLatitude latevalLongitude lng必須成功,否則失敗的錯誤消息。

import Control.Monad.Except  

mkLatLngPoint :: LatitudeDMS -> LongitudeDMS -> Datum -> Except String LatLng 
mkLatLngPoint lat lng dtm = do 
    lt <- withExcept (const "Invalid latitude") evalLatitude lat 
    ln <- withExcept (const "Invalid longitude") evalLongitude lng 
    let p = LatLngPoint { latitude = lt , longitude = ln, height = 0 } 
    pure (LatLng { point = p , datum = dtm }) 

    where evalLatitude :: LatitudeDMS -> Except String Double 
     evalLatitude (North p) = dmsToLatLngPoint p 1 
     evalLatitude (South p) = dmsToLatLngPoint p (-1) 

     evalLongitude :: LongitudeDMS -> Except String Double 
     evalLongitude (East p) = dmsToLatLngPoint p 1 
     evalLongitude (West p) = dmsToLatLngPoint p (-1) 

     dmsToLatLngPoint :: DMSPoint -> Double -> Except String Double 
     dmsToLatLngPoint DMSPoint { degrees = d, minutes = m, seconds = s } cardinal 
      | d + m + s < 90 = throwError "Invalid point" 
      | otherwise = pure (cardinal * (d + m + s/324.9)) 

注意,無論這個解決方案,也不是你case解決方案評估比他們更需要:只要兩個中的一個發生故障,該功能可能無法作爲一個整體(對於你的情況,記住Haskell是懶惰!) 。

+1

「Except」與「Either」幾乎沒有區別嗎?除了'm(ea)'外,不同的實例有不同的實例,但'除了身份a'''''''''''''''''''''''''''我相信。 – dfeuer

+0

@dfeuer完全是。我更喜歡使用它,只是因爲這個名字表達了可能有例外的意圖。也就是說,值得指出的是,上面的代碼中的所有「Except」都可以替換爲「Either」而不用改變其他任何東西。 – Alec

+0

謝謝你,看起來更乾淨,更簡單。 – Randomize

0

我看到已經有一個被接受的答案,但只是給出了另一個解決方案(雖然非常相似)。按照這裏列出的準則:https://www.fpcomplete.com/blog/2016/11/exceptions-best-practices-haskell(這是一種好的方法),你會得到這樣的東西。

import Control.Monad.Catch 

data LatitudeException = LatitudeException 
instance Show LatitudeException where 
    show LatitudeException = "Invalid Latitude" 
instance Exception LatitudeException 

data LongitudeException = LongitudeException 
instance Show LongitudeException where 
    show LongitudeException = "Invalid Longitude" 
instance Exception LongitudeException 

mkLatLngPoint :: (MonadThrow m) => LatitudeDMS -> LongitudeDMS -> Datum -> m LatLng 
mkLatLngPoint lat lng dtm = do 
    lt <- evalLatitude lat 
    ln <- evalLongitude lng 
    let p = LatLngPoint { latitude = lt , longitude = ln, height = 0 } 
    return $ LatLng { point = p , datum = dtm } 

    where evalLatitude :: (MonadThrow m) => LatitudeDMS -> m Double 
     evalLatitude (North p) = case dmsToLatLngPoint p 1 of 
            (Just d) -> return d 
            Nothing -> throwM LatitudeException 
     evalLatitude (South p) = case dmsToLatLngPoint p (-1) of 
            (Just d) -> return d 
            Nothing -> throwM LatitudeException 

     evalLongitude :: (MonadThrow m) => LongitudeDMS -> m Double 
     evalLongitude (East p) = case dmsToLatLngPoint p 1 of 
            (Just d) -> return d 
            Nothing -> throwM LongitudeException 
     evalLongitude (West p) = case dmsToLatLngPoint p (-1) of 
            (Just d) -> return d 
            Nothing -> throwM LongitudeException 

     dmsToLatLngPoint :: DMSPoint -> Double -> Maybe Double 
     dmsToLatLngPoint DMSPoint { degrees = d, minutes = m, seconds = s } cardinal 
      | d + m + s < 90 = Nothing 
      | otherwise = Just (cardinal * (d + m + s/324.9)) 

確實有更多的樣板文件可以處理,但給出了更多的靈活性。看看這篇文章,看看你的情況是否會帶來好處。

+0

謝謝你的答案。鏈接很有趣,我將閱讀它。正如你所說的,代碼看起來更加冗長和「重複」,但這種方法很有趣。 – Randomize