在Haskell中捕获异常

时间:2014-05-04 15:27:50

标签: haskell exception-handling

在我看来,Haskell中的异常只能在它们被抛出后立即捕获,并且不会像在Java或Python中那样传播。下面是一个简短的例子:

{-# LANGUAGE DeriveDataTypeable #-}

import System.IO
import Control.Monad
import Control.Exception
import Data.Typeable

data MyException = NoParseException String deriving (Show, Typeable)
instance Exception MyException

-- Prompt consists of two functions:
-- The first converts an output paramter to String being printed to the screen.
-- The second parses user's input.
data Prompt o i = Prompt (o -> String) (String -> i)

-- runPrompt accepts a Prompt and an output parameter. It converts the latter
-- to an output string using the first function passed in Prompt, then runs
-- getline and returns user's input parsed with the second function passed
-- in Prompt.
runPrompt :: Prompt o i -> o -> IO i
runPrompt (Prompt ofun ifun) o = do
        putStr (ofun o)
        hFlush stdout
        liftM ifun getLine

myPrompt = Prompt (const "> ") (\s -> if s == ""
    then throw $ NoParseException s
    else s)

handleEx :: MyException -> IO String
handleEx (NoParseException s) = return ("Illegal string: " ++ s)

main = catch (runPrompt myPrompt ()) handleEx >>= putStrLn

运行程序后,只需在输入任何内容时按[Enter]键,我就会在输出中看到Illegal string:。而是显示:prog: NoParseException ""。现在假设Prompt类型和runPrompt函数在模块外部的公共库中定义,并且不能更改为处理传递给Prompt构造函数的函数中的异常。如何在不更改runPrompt的情况下处理异常?

我考虑过将第三个字段添加到Prompt以便以这种方式注入异常处理函数,但这对我来说似乎很难看。有更好的选择吗?

2 个答案:

答案 0 :(得分:10)

您遇到的问题是因为您在纯代码中抛出异常:throw的类型为Exception e => e -> a。纯代码中的例外是不精确do not guarantee ordering with respect to IO operations。因此catch没有看到纯throw。要解决此问题,您可以使用evaluate :: a -> IO a,其中&#34;可用于对其他IO操作进行评估。 (来自文档)。 evaluate就像回归一样,但它同时强制进行评估。因此,您可以将liftM ifun getLine替换为evaluate . ifun =<< getline,这会强制ifunrunPrompt IO操作期间进行评估。 (回想一下liftM f mx = return . f =<< mx,所以这是相同的,但可以更好地控制评估。)并且不改变任何其他内容,你将得到正确的答案:

*Main> :main
> 
Illegal string: 

但实际上,这并不是我使用异常的地方。人们不会在Haskell代码中使用异常,特别是在纯代码中不会。我更愿意写Prompt,以便输入函数的潜在失败将在类型中编码:

data Prompt o i = Prompt (o -> String) (String -> Either MyException i)

然后,运行提示只会返回Either

runPrompt :: Prompt o i -> o -> IO (Either MyException i)
runPrompt (Prompt ofun ifun) o = do putStr $ ofun o
                                    hFlush stdout
                                    ifun `liftM` getLine

我们调整myPrompt以使用LeftRight代替throw

myPrompt :: Prompt a String
myPrompt = Prompt (const "> ") $ \s ->
             if null s
               then Left $ NoParseException s
               else Right s

然后我们使用either :: (a -> c) -> (b -> c) -> Either a b -> c来处理异常。

handleEx :: MyException -> IO String
handleEx (NoParseException s) = return $ "Illegal string: " ++ s

main :: IO ()
main = putStrLn =<< either handleEx return =<< runPrompt myPrompt ()

(另外,不相关,请注意:您会注意到我在这里做了一些风格上的改变。我唯一说的是真正重要的是使用null s,而不是s == "" 。)

如果您确实希望旧行为返回顶级,则可以编写runPromptException :: Prompt o i -> o -> IO i,将Left案例作为例外:

runPromptException :: Prompt o i -> o -> IO i
runPromptException p o = either throwIO return =<< runPrompt p o

我们不需要在此使用evaluate,因为我们正在使用throwIO,这是为了在IO次计算中抛出精确的异常。有了这个,您的旧main函数将正常工作。

答案 1 :(得分:2)

如果您查看myPrompt的类型,您会看到它是Prompt o String,即不在IO中。对于最小的修复:

{-# LANGUAGE DeriveDataTypeable #-}

import System.IO
import Control.Monad
import Control.Exception
import Data.Typeable

data MyException = NoParseException String deriving (Show, Typeable)
instance Exception MyException

-- Prompt consists of two functions:
-- The first converts an output paramter to String being printed to the screen.
-- The second parses user's input.
data Prompt o i = Prompt (o -> String) (String -> IO i)

-- runPrompt accepts a Prompt and an output parameter. It converts the latter
-- to an output string using the first function passed in Prompt, then runs
-- getline and returns user's input parsed with the second function passed
-- in Prompt.
runPrompt :: Prompt o i -> o -> IO i
runPrompt (Prompt ofun ifun) o = do
        putStr (ofun o)
        hFlush stdout
        getLine >>= ifun

myPrompt :: Prompt o String
myPrompt = Prompt (const "> ") (\s -> if s == ""
    then throw $ NoParseException s
    else return s)

handleEx :: MyException -> IO String
handleEx (NoParseException s) = return ("Illegal string: " ++ s)

main = catch (runPrompt myPrompt ()) handleEx >>= putStrLn

虽然Prompt o i e = Prompt (o -> String) (String -> Either i e)可能更合适。