【问题标题】:How to implement early exit / return in Haskell?如何在 Haskell 中实现提前退出/返回?
【发布时间】:2012-12-19 01:14:28
【问题描述】:

我正在将 Java 应用程序移植到 Haskell。 Java 应用程序的 main 方法遵循以下模式:

public static void main(String [] args)
{
  if (args.length == 0)
  {
    System.out.println("Invalid number of arguments.");

    System.exit(1);
  }

  SomeDataType d = getData(arg[0]);
  if (!dataOk(d))
  {
    System.out.println("Could not read input data.");

    System.exit(1);
  }

  SomeDataType r = processData(d);
  if (!resultOk(r))
  {
    System.out.println("Processing failed.");

    System.exit(1);
  }

  ...
}

所以我有不同的步骤,在每个步骤之后我可以退出并显示错误代码,或者继续执行下一个步骤。

我将其移植到 Haskell 的尝试如下:

main :: IO ()
main = do
         a <- getArgs
         if (null args)
           then do
                   putStrLn "Invalid number of arguments."
                   exitWith (ExitFailure 1)
           else do
                   -- The rest of the main function goes here.

使用此解决方案,我将拥有大量嵌套的if-then-else(一个用于原始 Java 代码的每个退出点)。

在 Haskell 中实现这种模式是否有更优雅/惯用的方式?一般来说,什么是 Haskell 惯用的方式来实现在 Java 等命令式语言中使用的提前退出/返回?

【问题讨论】:

  • 请从learnyouahaskell.com/a-fistful-of-monads 页面阅读Walking the Line 的示例。它给出了使用 Maybe 数据类型的 Monad 示例。一旦任何表达式的结果为Nothing,那么后面所有表达式的结果都是Nothing,就像您在失败点退出一样好。
  • @ManojR - 也许在这里并不适合,因为您还想知道失败的原因。
  • 经典的方法是将过程拆分为“信任”处理函数(假设参数正确)和“偏执”健全性检查函数(仅检查参数是否正确)...
  • 请不要使用length检查空列表。如果您确实需要进行简单检查,但更喜欢模式匹配,请使用null,因为它将检查与为列表元素指定名称相结合,以便您可以使用它们。
  • @C. A. 麦肯:谢谢。我通常使用nulllength 从 Java 原版中溜进来,我并没有太在意它,因为我希望参数列表相当短。但是,是的,null 绝对是正确的选择。

标签: java haskell control-structure


【解决方案1】:

一种方法是使用ErrorT monad 转换器。有了它,你可以把它当作一个普通的 monad,return,bind,所有这些好东西,但你也可以得到这个函数,throwError。这会导致您跳过以下计算,直到您到达 monadic 计算的末尾,或者当您调用 catchError 时。这是用于错误处理的,而不是用于任意退出 Haskell 中的函数。我建议这样做是因为您似乎正在这样做。

一个简单的例子:

import Control.Monad.Error
import System.Environment

data IOErr = InvalidArgs String | GenErr String deriving (Show)
instance Error IOErr where
  strMsg = GenErr --Called when fail is called
  noMsg  = GenErr "Error!"
type IOThrowsError = ErrorT IOErr IO

process :: IOThrowsError [String]
process = do
  a <- liftIO getArgs
  if length a == 0
  then throwError $ InvalidArgs "Expected Arguments, received none"
  else return a

main = do 
  result <- runErrorT errableCode
  case result of
     Right a -> putStrLn $ show a
     Left  e -> putStrLn $ show e
  where errableCode = do
    a <- process
    useArgs a

现在如果进程抛出错误,useArgs 将不会被执行。

【讨论】:

    【解决方案2】:

    这是我想出来的

    data ExtendedMaybe a = Just a | GenErr String 
    
    isWrongArgs :: [string] -> ExtendedMaybe [string]
    isWrongArgs p = if (length p == 0)
    then GenErr "Invalid number of arguments"
    else p
    
    getData :: ExtendedMaybe [string] -> ExtendedMaybe sometype 
    getData GenErr = GenErr
    getData [string] = if anything wrong return GenErr "could not read input data"
    
    processdata :: ExtendedMaybe sometype -> ExtendedMaybe sometype
    processdata GenErr = GenErr 
    
    main = do 
        a <- getArgs
        d <- isWrongArgs a
        r <- getData d
        f <- processdata r
    

    大致的想法是你有一个像 Maybe a 这样的数据类型,只是你有 GenErr 字符串而不是 Nothing,你在每个处理数据的函数中定义它。如果输入数据类型是 GenErr,只需返回它。否则检查数据中的错误并返回带有适当字符串的 GenErr。这可能不是完美的方式,但仍然是一种方式。这不会在确切的错误点退出,但保证在发生错误后不会发生太多事情。

    【讨论】:

    • 你的ExtendedMaybe类型和Either String基本一样
    【解决方案3】:

    在 Haskell 中使用与您尝试过的相同类型的条件逻辑的稍微更明智的方法可能如下所示:

    fallOverAndDie :: String -> IO a
    fallOverAndDie err = do putStrLn err
                            exitWith (ExitFailure 1)
    
    main :: IO ()
    main = do a <- getArgs
              case a of
                  [d] | dataOk d  -> doStuff $ processData d
                      | otherwise -> fallOverAndDie "Could not read input data."
                  _ -> fallOverAndDie "Invalid number of arguments."
    
    
    processData r 
        | not (resultOk r) = fallOverAndDie "Processing failed."
        | otherwise        = do -- and so on...
    

    在这种特殊情况下,鉴于exitWith 会终止程序无论如何,我们也可以完全省去嵌套条件:

    main :: IO ()
    main = do a <- getArgs
              d <- case a of
                       [x] -> return x
                       _   -> fallOverAndDie "Invalid number of arguments."
              when (not $ dataOk d) $ fallOverAndDie "Could not read input data."
              let r = processData d
              when (not $ resultOk r) $ fallOverAndDie "Processing failed."
    

    使用与以前相同的fallOverAndDie。这是对原始 Java 的更直接的翻译。

    在一般情况下,EitherMonad 实例让您可以用纯代码编写与上面后一个示例非常相似的内容。而是从这里开始:

    fallOverAndDie :: String -> Either String a
    fallOverAndDie = Left
    
    notMain x = do a <- getArgsSomehow x
                   d <- case a of
                            -- etc. etc.
    

    ...其余代码与我的第二个示例相同。当然,您也可以使用 String 以外的其他东西;要更忠实地重新创建IO 版本,您可以改用Either (String, ExitCode)

    此外,Either 的这种使用不限于错误处理——如果你有一些复杂的计算返回一个Double,使用Either Double Double 和与上面相同的单子样式,你可以使用Left 来尽早使用返回值退出,然后使用 either id id 之类的东西包装函数以折叠两个结果并获得单个 Double

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-09-10
      • 1970-01-01
      • 2020-01-06
      • 2016-04-30
      • 2018-08-11
      • 2016-09-20
      相关资源
      最近更新 更多