为什么throw和throwIO有区别?

时间:2019-08-07 19:58:51

标签: haskell exception

我正在努力牢固地了解异常,以便改进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实例吗?如果它取消任何同步异常,而不仅仅是以特定方式定义并以特定方式抛出的一小部分异常,会更好吗?

3 个答案:

答案 0 :(得分:6)

throwundefinederror的概括,它旨在在纯代码中引发异常。如果异常的值无关紧要(通常是大多数时间),则用符号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不是引发错误的程序-不是程序。无论您是否使用过IOErrorx都不是有效的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动作,并发现它实际上没有提供值,此时它可以停止并尝试<|>表达式的另一个“一半”。