我正在努力牢固地了解异常,以便改进conditional loop implementation。为此,我正在进行各种实验,扔东西,看看有什么被抓住。
这让我感到无休止:
% cat X.hs
module Main where
import Control.Exception
import Control.Applicative
main = do
throw (userError "I am an IO error.") <|> print "Odd error ignored."
% ghc X.hs && ./X
...
X: user error (I am an IO error.)
% cat Y.hs
module Main where
import Control.Exception
import Control.Applicative
main = do
throwIO (userError "I am an IO error.") <|> print "Odd error ignored."
% ghc Y.hs && ./Y
...
"Odd error ignored."
我认为替代方案应该完全忽略IO错误。 (不知道我是从哪里得到这个想法的,但是我当然不能提供在替代链中将被忽略的非IO异常。)因此,我认为自己可以手工制作并传递IO错误。 。事实证明,是否忽略它取决于包装与内容的关系:如果我throw
发生IO错误,则某种程度上不再 IO错误。
我完全迷路了。为什么这样工作?有意吗这些定义深入到了GHC内部模块。尽管我可以自己或多或少地理解不同代码片段的含义,但我很难看清整个画面。
如果很难预测,甚至应该使用这个Alternative实例吗?如果它取消任何同步异常,而不仅仅是以特定方式定义并以特定方式抛出的一小部分异常,会更好吗?
答案 0 :(得分:6)
throw
是undefined
和error
的概括,它旨在在纯代码中引发异常。如果异常的值无关紧要(通常是大多数时间),则用符号denoted表示“未定义的值”。
throwIO
是一个IO操作,它引发异常,但它本身并不是未定义的值。
throwIO
的文档说明了不同之处:
throw e `seq` x ===> throw e
throwIO e `seq` x ===> x
要注意的是,(<|>)
被定义为mplusIO
,它使用catchException
,这是catch
的严格变体。严格程度总结如下:
⟘ <|> x = ⟘
因此,您会发现x
变体出现异常(并且从未运行throw
)。
请注意,没有严格的规定,“未定义的动作”(即throw ... :: IO a
)的行为实际上就像是从catch
的角度出发抛出的动作:
catch (throw (userError "oops")) (\(e :: SomeException) -> putStrLn "caught") -- caught
catch (throwIO (userError "oops")) (\(e :: SomeException) -> putStrLn "caught") -- caught
catch (pure (error "oops")) (\(e :: SomeException) -> putStrLn "caught") -- not caught
答案 1 :(得分:5)
说你有
x :: Integer
这当然意味着x
应该是整数。
x = throw _whatever
那是什么意思?这意味着应该有一个Integer
,但是只有一个错误。
现在考虑
x :: IO ()
这意味着x
应该是不返回任何有用值的I / O执行程序。请记住,IO
值只是值。它们是恰好代表命令性程序的值。所以现在考虑
x = throw _whatever
这意味着应该在那里有一个执行I / O的程序,但实际上只是一个错误。 x
不是引发错误的程序-不是程序。无论您是否使用过IOError
,x
都不是有效的IO
程序。当您尝试执行程序时
x <|> _whatever
您必须执行x
来查看它是否引发错误。但是,您无法执行x
,因为它不是程序,这是一个错误。相反,一切都会爆炸。
这与
有很大不同x = throwIO _whatever
现在x
是一个有效程序。这是一个总是会引发错误的有效程序,但它仍然是可以实际执行的有效程序。当您尝试执行
x <|> _whatever
现在,执行x
,丢弃产生的错误,并在其位置执行_whatever
。您还可以想到计算程序/确定要执行的程序与实际执行程序之间的区别。 throw
在计算要执行的程序时引发错误(这是“纯异常”),而throwIO
在执行过程中引发错误(这是“纯异常”)。这也解释了它们的类型:throw
返回任何类型,因为可以对所有类型进行“计算”,但是throwIO
限于IO
,因为只能执行程序。
由于您可以捕获执行IO
程序时发生的纯异常,这一事实使情况更加复杂。我认为这是设计折衷。从理论上讲,您应该不能捕获纯异常,因为应该总是将它们的存在指示为程序员错误,但这可能会很尴尬,因为那样您就只能处理外部错误。 ,而程序员错误会导致一切崩溃。如果我们是完美的程序员,那会很好,但事实并非如此。因此,您可以捕获纯异常。
is :: [Int]
is = []
-- fails, because the print causes a pure exception
-- it was a programmer error to call head on is without checking that it,
-- in fact, had a head in the first place
-- (the program on the left is not valid, so main is invalid)
main1 = print (head is) <|> putStrLn "Oops"
-- throws exception
-- catch creates a program that computes and executes the program print (head is)
-- and catches both impure and pure exceptions
-- the program on the left is invalid, but wrapping it with catch
-- makes it valid again
-- really, that shouldn't happen, but this behavior is useful
main2 = print (head is) `catch` (\(_ :: SomeException) -> putStrLn "Oops")
-- prints "Oops"
答案 2 :(得分:3)
此答案的其余部分可能并不完全正确。但从根本上说,区别是:throwIO
终止并返回一个IO
动作,而throw
则不终止。
一旦您尝试评估throw (userError "...")
,程序就会中止。 <|>
永远没有机会查看其第一个参数来决定是否应评估第二个参数;实际上,它永远不会获取第一个参数,因为throw
没有返回值。
对于throwIO
,<|>
不会进行任何评估;它正在创建一个新的IO
动作,当它执行 时,将首先查看其第一个参数。运行时可以“安全地”执行IO
动作,并发现它实际上没有提供值,此时它可以停止并尝试<|>
表达式的另一个“一半”。