1

我正在尝试编写一个程序,它在命令行上接受两个整数并用它们做一些有趣的事情。我想尽可能容易和命令地编写整数的读取/解析,因为它应该是相对简单的代码。

我面临的问题是在 Haskell 中处理错误并不是那么简单。似乎在 Haskell 中经常使用模式匹配。这似乎使代码比命令式版本更难遵循。

该程序将像这样运行(在此示例中,它只是将两个数字相加):

$ ./my_prog
ERROR: Must run like `./my_prog NUM_A NUM_B`.
$ ./my_prog cat 1
ERROR: Could not parse NUM_A "cat" as integer
$ ./my_prog 10 dog
ERROR: Could not parse NUM_B "dog" as integer
$ ./my_prog 10 1
11

这是我想在命令式伪 Python 中做的事情:

function die (errorMessage):
    print("ERROR: %s" % errorMessage)
    sys.exit(1)

function main ():
    if len(sys.argv) != 2:
        die("Must run program like `%s NUM_A NUM_B`" % sys.progname)

    num_a = 0
    num_b = 0

    try:
        num_a = int(sys.argv[0])
    except:
        die("Could not parse NUM_A \"%s\" as integer" % sys.argv[0])

    try:
        num_b = int(sys.argv[1])
    except:
        die("Could not parse NUM_B \"%s\" as integer" % sys.argv[1])

     doSomethingInteresting(num_a, num_b)

function doSomethingInteresting (num_a, num_b):
    print(num_a + num_b)

在 python 中,您基本上可以从上到下读取 main 函数,并且所有错误处理都很简单。 有没有办法在 Haskell 中实现这种简单、直接的错误处理,而无需进行多个模式匹配?

这是我提出的执行相同任务的 Haskell 代码,但由于多个模式匹配部分,它似乎比 Python 代码复杂得多。

module Main ( main
            )
            where

import System.Environment (getArgs, getProgName)
import System.Exit (ExitCode(..), exitWith)
import Text.Read (readMaybe)

die :: String -> IO a
die err = do putStrLn $ "ERROR: " ++ err
             exitWith (ExitFailure 1)

main :: IO ()
main = do
        args <- getArgs
        progName <- getProgName
        case args of
            [strNumA, strNumB] -> do
                let maybeNumA = readMaybe strNumA :: Maybe Int
                    maybeNumB = readMaybe strNumB :: Maybe Int
                checkMaybeArgs strNumA maybeNumA strNumB maybeNumB
            _ -> die ("Must run like `" ++ progName ++ " NUM_A NUM_B`.")
    where
        checkMaybeArgs :: String -> Maybe Int -> String -> Maybe Int -> IO ()
        checkMaybeArgs badStrNumA Nothing _ _ =
            die ("Could not parse NUM_A \"" ++ badStrNumA ++ "\" as integer")
        checkMaybeArgs _ _ badStrNumB Nothing =
            die ("Could not parse NUM_B \"" ++ badStrNumB ++ "\" as integer")
        checkMaybeArgs _ (Just numA) _ (Just numB) = doSomethingInteresting numA numB

        doSomethingInteresting :: Int -> Int -> IO ()
        doSomethingInteresting numA numB = print $ numA + numB

(此外,如果我的 Haskell 风格有任何其他问题,我将非常感谢任何更正。)

编辑:我最近发现一篇博客文章讨论了在 Haskell 中处理异常的许多不同方法。这有点相关:

http://www.randomhacks.net/articles/2007/03/10/haskell-8-ways-to-report-errors

4

4 回答 4

6

这是我写这个的方式(不使用任何外部库)

import Text.Read
import Text.Printf
import System.Environment
import Control.Monad
import System.Exit


parsingFailure :: String -> String -> IO a
parsingFailure name val = printf "ERROR: Couldn't parse %s : %s as an integer\n" 
                                  name val >> exitWith (ExitFailure 1)

validateArgs :: [String] -> IO (Integer, Integer)
validateArgs [a, b] = liftM2 (,) (parse "VAL A" a) (parse "VAL B" b)
  where parse name s = maybe (parsingFailure name s) return $ readMaybe s
validateArgs _      = putStrLn "Wrong number of args" >> exitWith (ExitFailure 1)

main :: IO ()
main = do
  (a, b) <- getArgs >>= validateArgs
  print $ a + b

有趣的一点当然是validateArgs。首先我们做一个单一的模式匹配,但从那时起我们只使用maybe组合器很好地抽象出我们的模式匹配。这导致更清晰的代码 IMO。组合器采用maybe默认值 b和延续a -> b并展开Maybe ab. 在这种情况下,我们的默认值是解析失败,我们的延续注入aIOmonad 中。

于 2013-11-27T05:07:07.250 回答
4

尽管我喜欢 @jozefgs 解决方案的简洁性,但我不喜欢它使用exitWith. 当然,对于这样一个小例子来说它工作得很好,但总的来说,我认为像这样“过早”终止程序是个坏主意。这确实是我不喜欢 Python 版本的属性。无论如何,我的解决方案如下,我将在下面解释它的几个部分。

import System.Environment (getProgName, getArgs)
import Text.Read (readMaybe)

doSomethingInteresting :: Int -> Int -> IO ()
doSomethingInteresting a b = print (a + b)

readWith :: Read a => (String -> e) -> String -> Either e a
readWith err str = maybe (Left $ err str) return $ readMaybe str


main = do
  progName <- getProgName
  args     <- getArgs

  either putStrLn id $ do
    (a,b) <- case args of
               [a,b] -> return (a,b)
               _     -> Left ("Must run program like `" ++ progName ++ " NUM_A NUM_B`")

    num_a <- readWith (\s -> "Could not parse NUM_A \"" ++ s ++ "\" as integer") a
    num_b <- readWith (\s -> "Could not parse NUM_B \"" ++ s ++ "\" as integer") b

    return $ doSomethingInteresting num_a num_b

我知道其中有一个模式匹配,但这确实是表达我想要的最干净的方式。如果列表有两个元素,我希望它们脱离它。如果没有,我想要一条错误消息。没有比这更简洁的表达方式了。

但这里还有一些我想强调的其他事情。

单一出口点

首先,我已经提到过,程序没有几个“退出点”,即它不会在某事中间终止程序。想象一下,如果该main函数不是主要函数,而是在更深的某个地方,并且您在上面的函数中打开了一些文件或诸如此类的东西。你会想以正确的方式关闭文件等等。这就是为什么我选择不终止程序,而是让它运行它,如果它没有数字就不要进行计算。

仅仅终止程序很少是一个好主意。通常,您有要保存的内容、要关闭的文件、要恢复的状态等等。

Either注释错误

对于初学者来说可能不是很明显,但readWith基本上所做的就是尝试read某事,如果成功,它会返回Right <read value>。如果失败,它将返回Left <error message>,其中错误消息可能取决于它尝试读取的字符串。

所以,在这个节目中,

λ> readWith errorMessage "15"
Right 15

尽管

λ> readWith errorMessage "crocodile"
Left "Cannot read \"crocodile\" as an integer, dummy!"

当您想要传输一个值但在此过程中可能会发生错误时,该Either类型非常有用,并且您希望保留错误消息,直到您知道如何处理该错误。社区共识是Right应该指出“正确”的值,并且Left应该指出发生了一些错误。

Eithervalues 是一种更受控制(阅读:更好)的异常系统。

do语法是你的朋友

do语法对于处理错误非常方便。一旦任何计算结果产生一个Left值,Haskell 将do使用Left. 这意味着do在这种情况下,整个内部块将导致类似于

Right (print 41)

或类似的东西

Left "Could not parse NUM_A \"crocodile\" as integer"

然后该either函数确保打印错误消息或仅返回作为值的IO操作Right。来自 Python 的你可以将print动作存储为值可能很奇怪,但这在 Haskell 中很正常。我们说 I/O 动作是Haskell 中的一等公民。换句话说,我们可以传递它们并将它们存储在数据结构中,然后我们自己决定何时执行它们。

于 2013-11-27T09:35:15.343 回答
1

这是我的尝试:

module Main where

import Text.Read (readMaybe)
import System.Environment (getProgName, getArgs)

main = getArgs >>= \argv -> case argv of
  [x, y] -> case (readMaybe x, readMaybe y) of
    (Nothing, _      ) -> error $
      "ERROR: Could not parse NUM_A " ++ show x ++ " as integer"
    (_      , Nothing) -> error $
      "ERROR: Could not parse NUM_B " ++ show y ++ " as integer"
    (Just a , Just b ) -> print $ a + b
  _ -> do
    pname <- getProgName
    error $ "ERROR: Must run like `" ++ pname ++ " NUM_A NUM_B`."

请注意如何在元组上进行模式匹配以一次匹配多个表达式,从而避免需要多个嵌入的 case 表达式。

于 2013-11-27T05:26:34.503 回答
0

这是我的看法:

import System.Environment
import Text.Printf
import Text.Read


main :: IO ()
main = do
  args <- getArgs
  either putStrLn (uncurry doSomethingInteresting) $ parseArgs args


doSomethingInteresting :: Int -> Int -> IO ()
doSomethingInteresting a b = print $ a + b


parseArgs :: [String] -> Either String (Int, Int)
parseArgs [a, b] = do
  a' <- parseArg "A" a
  b' <- parseArg "B" b
  return (a', b')
parseArgs _ = fail "Wrong number of args"


parseArg :: String -> String -> Either String Int
parseArg name arg
  | Just x <- readMaybe arg = return x
  | otherwise = fail $ printf "Could not parse NUM_%s \"%s\" as integer" name arg

或者对于 Applicative 粉丝,另一种写作方式parseArgs

parseArgs :: [String] -> Either String (Int, Int)
parseArgs [a, b] = (,) <$> parseArg "A" a <*> parseArg "B" b
parseArgs _ = fail "Wrong number of args"
于 2013-11-27T12:21:24.680 回答