10

我正在将 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 这样的命令式语言中使用的提前退出/返回?

4

3 回答 3

7

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 的更直接的翻译。

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

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不限于错误处理——如果你有一些复杂的计算返回 a Double, usingEither Double Double和与上面相同的单子样式,你可以使用Left返回值提前退出,然后使用类似的东西包装函数either id id折叠两个结果并得到一个Double

于 2012-12-20T17:58:07.890 回答
5

一种方法是使用ErrorTmonad 转换器。有了它,你可以把它当作一个普通的 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 将不会被执行。

于 2012-12-20T11:23:10.723 回答
0

这就是我想出的

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。这可能不是完美的方式,但仍然是一种方式。这不会在确切的错误点退出,但保证在发生错误后不会发生太多事情。

于 2012-12-20T14:04:42.787 回答