【问题标题】:Error handling without pattern matching in HaskellHaskell 中没有模式匹配的错误处理
【发布时间】:2013-11-27 04:50:30
【问题描述】:

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

我面临的问题是在 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中你基本上可以从上到下读取m​​ain函数,所有的错误处理都很简单。 有没有办法在 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

【问题讨论】:

    标签: python haskell coding-style pattern-matching control-structure


    【解决方案1】:

    我是这样写的(不使用任何外部库)

    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 -&gt; b 并将 Maybe a 展开为 b。在这种情况下,我们的默认值是解析失败,我们的延续将a 注入到IO monad。

    【讨论】:

    • 如果您确实使用了外部库,这将如何改变?
    • @illabout 我可能会使用适当的命令行参数解析库。我很懒,这是其他人已经完成的工作。对于这么小的东西,它可能不值得,但仍然
    • 我曾想过使用 optparse-applicative 之类的东西,但我觉得对于这个小例子来说太过分了。
    • @illabout 够公平的。如果我已经回答了您的问题,您可以通过我的回答勾选复选标记,将此主题标记为“已解决”
    • 谢谢。对于任何不同的答案,我会再等一会儿,但我真的很喜欢你的答案。假设我没有得到更好的答案,我会选择你的。
    【解决方案2】:

    虽然我喜欢@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 &lt;read value&gt;。如果失败,它将返回Left &lt;error message&gt;,错误消息可能取决于它尝试读取的字符串。

    所以,在这个程序中,

    λ> readWith errorMessage "15"
    Right 15
    

    同时

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

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

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

    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 中的一等公民。换句话说,我们可以传递它们并将它们存储在数据结构中,然后我们自己决定何时执行它们。

    【讨论】:

    • 这是一个非常好的答案。如果我能接受两个答案,我会的。我同意不要提前退出程序,但在我的具体情况下(以及@jozefg 的回答),我认为这是可以接受的,因为没有什么需要清理的。但在几乎所有其他情况下,您的答案将是更好的答案。感谢您抽出宝贵时间写出精彩的解释。它帮助我理解了在 Maybe 和 Either 单子中处理“Nothing”和“Left”。
    【解决方案3】:

    这是我的尝试:

    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 表达式。

    【讨论】:

    • 这仍然使用两个模式匹配(两个 case 语句),这是我想避免的,但它仍然比我编写的代码更好。恕我直言,这段代码(和我的代码)不像 python 代码那样从上到下阅读。
    • 确实如此。一般来说,在 Haskell 中,我们更喜欢这种“嵌套”决策,使用模式匹配树,而不是 Python 代码中更命令式的风格。这可能是因为静态检查更容易;当您匹配[x, y] 而不是检查length == 2 时,编译器保证您不会意外索引不在列表中的元素。
    • @Onyxite 我不同意,我阅读的大多数代码都没有嵌套 case 表达式,因为它更难阅读。它与类型检查完全正交。大多数代码将事情分解成小的(
    • 我同意@jozefg,嵌套案例很难阅读,我倾向于远离它们。虽然这个解决方案很简洁,但它对 OP 主要关注的模式匹配混乱没有什么改善。
    • @jozefg,这不仅仅是类型检查,也是绑定值的范围。 Python 代码检查if len(sys.argv) != 2:,然后引用argv[0]argv[1],要求程序员在索引数组之前验证检查是否发生。 Haskell 的case 确保您不会意外引用第一个不存在的元素,因为嵌套创建了变量的范围。
    【解决方案4】:

    这是我的看法:

    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"
    

    【讨论】:

      猜你喜欢
      • 2016-09-03
      • 1970-01-01
      • 2017-10-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多