【问题标题】:Elegant haskell case/error handling in sequential monads顺序单子中优雅的haskell案例/错误处理
【发布时间】:2012-11-06 14:16:43
【问题描述】:

由于我之前在other question中过于简单化了,这里我想举一个更清楚的例子。

如何处理必须以顺序方式检查某些条件而不嵌套多个案例的情况?对于“顺序方式”,我的意思是获取一个值(例如从标准输入),检查该值是否符合特定条件,并根据结果获取另一个值等等。

例子:

sequen :: IO String
sequen = do
  a <- getLine
  case a of
    "hi" -> do
      putStrLn "hello!"
      b <- getLine
      case b of
        "how are you?" -> do
          putStrLn "fine, thanks"
          return "nice conversation"
        _ -> return "error 2"
    _ -> return "error 1"

我知道有更好的方法来编写这样的聊天机器人,它应该只是展示问题的顺序性质。如您所见,对于每个嵌套大小写,代码也会缩进更深。

有没有办法更好地构造这样的代码?我正在考虑在一个地方处理“错误”并描述“成功路径”,而无需将错误处理分布在所有地方。

【问题讨论】:

    标签: haskell


    【解决方案1】:

    当然。这正是 EitherT 的用途。您可以从 eitherT 包中的 Control.Monad.Trans.Either 获取它。

    import Control.Monad.Trans.Class
    import Control.Monad.Trans.Either
    
    main = do
        e <- runEitherT $ do
            a <- lift getLine
            case a of
                "hi" -> lift $ putStrLn "hello!"
                _    -> left 1
            b <- lift getLine
            case b of
                "how are you?" -> lift $ putStrLn "fine, thanks!"
                _              -> left 2
            return "nice conversation"
        case e of
            Left  n   -> putStrLn $ "Error - Code: " ++ show n
            Right str -> putStrLn $ "Success - String: " ++ str
    

    EitherT 在遇到left 语句时中止当前代码块,人们通常使用它来指示错误情况。

    内部块的类型是EitherT Int IO String。当你runEitherT 它时,你会得到IO (Either Int String)Left 类型对应于它以 left 失败的情况,Right 值表示它成功到达块的末尾。

    【讨论】:

    • 太棒了。正是我想知道的。谢谢!
    • 在 EitherT 的笔记上,我写了一篇博文,颇受好评:ocharles.org.uk/blog/posts/2012-07-24-in-praise-of-EitherT.html
    • @ocharles 我前段时间读过,认为它应该是我问题的解决方案,但直到现在我才能应用它。你可以在你的帖子中链接到这个问题:)
    • @Gabriel 您是否打算让 EitherT 完全取代 ErrorT 用例?我已经使用 ErrorT 很长时间了,但有时会觉得为我的所有异常类声明 Error 实例很烦人。
    • 我真的希望 EitherT 替换 ErrorT 用于所有可能的用例。问题是 Edward Kmett 和我很难说服 Ross Patterson (a) 从 ErrorT 中删除 Error 约束或 (b) 将 EitherT 添加到 transformers。这就是目前的阻碍。
    【解决方案2】:

    不久前,我写了一系列帖子,回顾了我自己对 EitherEitherT 类型的了解。你可以在这里阅读:http://watchchrislearn.com/blog/2013/12/01/working-entirely-in-eithert/

    我使用errors 包来获得一堆使用EitherT 的好帮手(例如leftright 函数返回LeftRight 的提升版本)。

    通过将潜在的失败条件提取到它们自己的帮助程序中,您可以使代码的主线完全按顺序读取,而无需 case 语句检查结果。

    从那篇文章中,您可以看到runEitherT 部分是如何连续工作的,它恰好具有EitherT 的故障机制。显然,这段代码是相当人为地展示了MaybeT 如何在EitherT 内部发挥作用。在真正的代码中,它只是你想要讲述的故事,最后只有一个 Left/Right

    import Control.Error
    import Control.Monad.Trans
    
    -- A type for my example functions to pass or fail on.
    data Flag = Pass | Error
    
    main :: IO ()
    main = do
      putStrLn "Starting to do work:"
    
      result <- runEitherT $ do
          lift $ putStrLn "Give me the first input please:"
          initialText <- lift getLine
          x <- eitherFailure Error initialText
    
          lift $ putStrLn "Give me the second input please:"
          secondText <- lift getLine
          y <- eitherFailure Pass (secondText ++ x)
    
          noteT ("Failed the Maybe: " ++ y) $ maybeFailure Pass y
    
      case result of
        Left  val -> putStrLn $ "Work Result: Failed\n " ++ val
        Right val -> putStrLn $ "Work Result: Passed\n " ++ val
    
      putStrLn "Ok, finished. Have a nice day"
    
    eitherFailure :: Monad m => Flag -> String -> EitherT String m String
    eitherFailure Pass  val = right $ "-> Passed " ++ val
    eitherFailure Error val = left  $ "-> Failed " ++ val
    
    maybeFailure :: Monad m => Flag -> String -> MaybeT m String
    maybeFailure Pass  val = just $ "-> Passed maybe " ++ val
    maybeFailure Error _   = nothing
    

    【讨论】:

    • 很好的例子!感谢您指出您可以将案例放在单独的功能中。使 main 函数更易于阅读。
    【解决方案3】:

    由于您必须在IO monad 中,因此最好使用IO monad 的错误处理功能,而不是在IO 之上堆叠错误monad。它避免了所有繁重的lifting:

    import Control.Monad ( unless )
    import Control.Exception ( catch )
    import Prelude hiding ( catch )
    import System.IO.Error ( ioeGetErrorString )
    
    main' = do
      a <- getLine
      unless (a == "hi") $ fail "error 1"
      putStrLn "hello!"
      b <- getLine
      unless (b == "how are you?") $ fail "error 2"
      putStrLn "fine, thanks"
      return "nice conversation"
    
    main = catch main' $ return . ioeGetErrorString
    

    在这种情况下,您的错误只是Strings,由IOfail 作为userError 抛出。如果要抛出其他类型,则需要使用throwIO 而不是fail

    【讨论】:

      【解决方案4】:

      在某些时候the EitherT package was deprecated(尽管transformers-either 提供了类似的API)。幸运的是,EitherT 有一个替代方案,甚至不需要安装单独的包。

      标准的 Haskell 安装带有 Control.Monad.Trans.Except 模块(来自与 GHC 捆绑的 transformers 包),其行为几乎与 EitherT 相同。结果代码几乎Gabriella Gonzalez's answer 中的代码相同,但使用runExceptT 代替runEitherTthrowE 代替left

      import Control.Monad.Trans.Class
      import Control.Monad.Trans.Except
      
      main = do
          e <- runExceptT $ do
              a <- lift getLine
              case a of
                  "hi" -> lift $ putStrLn "hello!"
                  _    -> throwE 1
              b <- lift getLine
              case b of
                  "how are you?" -> lift $ putStrLn "fine, thanks!"
                  _              -> throwE 2
              return "nice conversation"
          case e of
              Left  n   -> putStrLn $ "Error - Code: " ++ show n
              Right str -> putStrLn $ "Success - String: " ++ str
      

      (请注意,前面提到的transformers-either 包实际上是ExceptT 的包装器,旨在提供与仍然使用EitherT 的代码的兼容性。)

      【讨论】:

        【解决方案5】:

        警告:Haskell 新手正在回答。

        您可以使用 Maybe monad 避免这种楼梯。 this chapter开头的好例子

        但是,由于您返回错误代码,因此您需要与单子 Either(可能有一个)类似的东西。

        基本思想是,一旦出现“Left 1”错误,您将缩短任何未来的步骤(因为延迟评估)。

        【讨论】:

        • 啊,根据下面的@Gabriel,我知道我走在了正确的轨道上。
        • 因为懒惰评估?还是因为Either的monad实例的定义?
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-01-19
        • 2012-06-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多