是不是 Monad?

Either Monad or not?

在此代码中:

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 }

但我认为是丑陋和冗长的。

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

我会为此使用 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 很懒!)。

我看到已经有一个可接受的答案,但只是给出了另一个解决方案(尽管非常相似)。遵循此处概述的指南: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))

确实有更多的样板文件需要处理,但提供了更多的灵活性。查看这篇文章,看看对您的情况是否有好处。