在我看来,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
以便以这种方式注入异常处理函数,但这对我来说似乎很难看。有更好的选择吗?
答案 0 :(得分:10)
您遇到的问题是因为您在纯代码中抛出异常:throw
的类型为Exception e => e -> a
。纯代码中的例外是不精确和do not guarantee ordering with respect to IO
operations。因此catch
没有看到纯throw
。要解决此问题,您可以使用evaluate :: a -> IO a
,其中"可用于对其他IO操作进行评估。 (来自文档)。 evaluate
就像回归一样,但它同时强制进行评估。因此,您可以将liftM ifun getLine
替换为evaluate . ifun =<< getline
,这会强制ifun
在runPrompt
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
以使用Left
和Right
代替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)
可能更合适。