如何在哈斯克尔中执行控制流



我将举一个例子来说明我想立即做什么。

version1 :: IO ()
version1 =
  if boolCheck
     then case maybeCheck of
            Nothing -> putStrLn "Error: simple maybe failed"
            Just v  -> case eitherCheck of
                         Left  e -> putStrLn $ "Error: " ++ show e
                         Right w -> monadicBoolCheck v >>= case
                                      False -> putStrLn "Error: monadic bool check failed"
                                      True  -> print "successfully doing the thing"
    else putStrLn "simple bool check failed"

基本上,我想在多项检查结果为阳性的情况下"做一件事"。每当单次检查结果为阴性时,我都想保留有关违规检查的信息并中止任务。在现实生活中,这些支票有不同的类型,因此我称它们为它们

boolCheck        :: Bool
maybeCheck       :: Maybe a
eitherCheck      :: Show a => Either a b
monadicBoolCheck :: Monad m => m Bool

这些只是例子。随意也想想一元也许,EitherT或单例列表,我提取head并在它不是单例时失败。

现在我正在尝试改进上述实现,Either monad 浮现在我的脑海中,因为它具有因错误消息而中止的概念。

version2 :: IO ()
version2 = do
  result <- runEitherT $ do
    if boolCheck
       then pure ()
       else left "simple bool check failed"
    v <- case maybeCheck of
           Just x  -> pure x
           Nothing -> left "simple maybe check failed"
    w <- hoistEither . mapLeft show $ eitherCheck
    monadicBoolCheck v >>= case
      True  -> pure ()
      False -> left  "monadic bool check failed"
  case result of
    Left  msg -> putStrLn $ "Error: " ++ msg
    Right _   -> print "successfully doing the thing"

虽然我更喜欢version2,但可读性的提高可能是微不足道的。在添加进一步检查时,版本2更胜一筹。

有没有一种最终优雅的方式来做到这一点?

我不喜欢的:

1)我部分滥用了Either monad,我实际上所做的更像是一个Maybe monad,带有Just卷和Nothing在 monadic 绑定中切换

2)将支票转换为Either需要相当详细地使用case或转换函数(如hoistEither)。

提高可读性的方法可能是:

1) 定义辅助函数以允许类似代码

v <- myMaybePairToEither "This check failed" monadicMaybePairCheck
monadicMaybePairCheck :: Monad m => m (Maybe x, y)
...
myMaybePairToEither :: String -> m (Maybe x, y) -> EitherT m e z
myMaybePairToEither _   (Just x, y)  = pure $ f x y
myMaybePairToEither msg (Nothing, _) = left msg

2)始终如一地使用显式大小写,甚至不使用hoistEither

3)定义我自己的单子以阻止Either滥用......我可以提供所有的转换功能(如果没有人已经做过这样的事情)

4) 尽可能使用maybeeither

5) ... ?

使用 maybeeithermtl 包。顺便说一下,eitherCheck :: Show a => Either a bShow a约束可能不是你想要的:它允许调用者选择他们想要的任何类型,只要该类型实现了Show a。您可能打算a调用方只能调用该值的show类型。可能!

{-# LANGUAGE FlexibleContexts #-}
newtype Error = Error String
gauntlet :: MonadError Error m => m ()
gauntlet = do
  unless boolCheck (throw "simple bool check failed")
  _ <- maybe (throw "simple maybe check failed") pure maybeCheck
  _ <- either throw pure eitherCheck
  x <- monadicBoolCheck
  unless x (throw "monadic bool check failed")
  return ()
  where
    throw = throwError . Error
version2 :: IO ()
version2 =
  putStrLn (case gauntlet of
              Left (Error e) ->
                "Error: " ++ e
              Right _ ->
                "successfully doing thing")

"定义辅助函数"正是我处理这个问题的方式。错误库已经提供了许多错误库,但可能除了满足Bool函数。对于那些我只会使用 when/unless .

当然,在可能的情况下,您应该将调用的操作提升为适当的多态性,以便不需要转换。

所以我可能会首先将您的version2重新加工成类似

import Control.Monad.Trans
import Control.Monad.Trans.Either hiding (left, right)
import Control.Monad
import Control.Applicative
import Control.Arrow
version3 :: IO ()
version3 = eitherT onFailure onSuccess $ do
    guard boolCheck <|> fail "simple bool check failed"
    v <- hoistEither $ maybe (Left "simple maybe check failed") Right maybeCheck
    w <- hoistEither . left show $ eitherCheck
    lift (guard =<< monadicBoolCheck v) <|> fail "monadic boolcheck failed"
  where
    onFailure msg = putStrLn $ "Error: "++msg
    onSuccess _   = print "successfully doing the thing"

我觉得这更具可读性,但仍然有点尴尬,所以如果我做了很多像这样的代码,我会介绍一些帮助程序:

version4 :: IO ()
version4 = eitherT onFailure onSuccess $ do
    failUnless "simple bool check failed" boolCheck
    v <- hoistMaybe "simple maybe check failed" maybeCheck
    w <- hoistEitherWith show eitherCheck
    failUnless "monadic boolcheck failed" =<< lift (monadicBoolCheck v)
  where
    onFailure msg = putStrLn $ "Error: "++msg
    onSuccess _   = print "successfully doing the thing"
failUnless :: Monad m => String -> Bool -> m ()
failUnless _ True = return ()
failUnless msg _ = fail msg
hoistMaybe :: Monad m => e -> Maybe a -> EitherT e m a
hoistMaybe err = hoistEither . maybe (Left err) Right
hoistEitherWith :: Monad m => (e -> e') -> Either e a -> EitherT e' m a
hoistEitherWith f = hoistEither . left f

为了在这里获得所有可能的选项,请查看以下要点:

https://gist.github.com/rubenmoor/c390901247e4e7bb97cf

它定义了几个辅助函数,基本上将maybeeither等与throwError结合起来,并产生这样的代码。

gauntlet :: MonadError Error m => m (a, b, c)
gauntlet = do
    assertTrue boolCheck $ Error "simple bool check failed"
    v <- assertJust maybeCheck $ Error "simple maybe check failed"
    assertNothing maybeCheck' $ Error . show
    w <- assertRight eitherCheck $ Error . show
    b <- monadicBoolCheck
    assertTrue b $ Error "monadic bool check failed"
    x <- assertSingletonList list $ Error "list not singleton"
    pure (v, w, x)
version3 :: IO ()
version3 = putStrLn $
  case gauntlet of
    Left  (Error e) -> "Error: " ++ e
    Right result    -> "successfully doing thing with result"