无论是Monad还是没有?

时间:2017-01-02 16:19:03

标签: haskell either

在此代码中:

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情况。我想过使用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 }

但我认为这是丑陋而冗长的。

如何改进代码(包括更改类型数据)?

2 个答案:

答案 0 :(得分: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 :(得分: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))

肯定会有更多的样板来处理,但会提供更多的灵活性。查看文章,看看您的情况是否有益处。