为什么 throw 和 throwIO 有区别?

Why is there difference between throw and throwIO?

我正在努力牢牢掌握异常情况,以便提高我的 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."

我认为 Alternative 应该完全忽略 IO 错误。 (不确定我从哪里得到这个想法,但我当然不能提供在替代链中会被忽略的非 IO 异常。) 所以我想我可以手工制作和交付IO 错误。事实证明,它是否被忽略取决于包装和内容:如果我 throw 一个 IO 错误,它在某种程度上 不再是 一个 IO 错误。

我完全迷路了。为什么会这样?是故意的吗?这些定义深入到 GHC 内部模块;虽然我自己或多或少可以理解不同的代码片段的含义,但我很难看到全貌。

如果很难预测,是否应该使用这个替代实例?如果它使任何同步异常静音,而不只是以特定方式 定义 抛出 的一小部分异常,这不是更好吗?具体方法?

此答案的其余部分可能不完全正确。但从根本上说,区别在于:throwIO 终止并且 return 是一个 IO 操作,而 throw 终止。


一旦您尝试计算 throw (userError "..."),您的程序就会中止。 <|> 从来没有机会查看它的第一个参数来决定是否应该评估第二个参数;事实上,它从来没有 得到 第一个参数,因为 throw 没有 return 一个值。

对于 throwIO<|> 没有评估任何东西;它正在创建一个新的 IO 动作,当它 does 被执行时,将首先查看它的第一个参数。运行时可以 "safely" 执行 IO 操作并看到它实际上没有提供值,此时它可以停止并尝试 [=14] 的另一个 "half" =] 表达式.

throwundefinederror 的概括,它的意思是在纯代码中抛出异常。当异常的值无关紧要时(大多数情况下),它用符号 ⟘ 表示 "undefined value".

throwIO 是一个抛出异常的 IO 操作,但它本身不是一个未定义的值。

throwIO 的文档因此说明了差异:

throw e   `seq` x  ===> throw e
throwIO e `seq` x  ===> x

要注意的是 (<|>) 被定义为 mplusIO which uses catchException,这是 catch 的严格变体。严格程度总结如下:

⟘ <|> x = ⟘

因此在 throw 变体中出现异常(并且 x 永远不会 运行)。

请注意,不严格地说,"undefined action"(即 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

假设你有

x :: Integer

这意味着x当然应该是一个整数。

x = throw _whatever

这是什么意思?这意味着应该有一个 Integer,但实际上只是一个错误。

现在考虑

x :: IO ()

这意味着x应该是一个I/O-performing程序,returns没有有用的价值。请记住,IO 值只是 。它们是恰好代表命令式程序的值。所以现在考虑

x = throw _whatever

这意味着那里应该有一个 I/O-performing 程序,但实际上只是一个错误。 x 不是一个会抛出错误的程序—— 没有程序。无论您是否使用过 IOErrorx 都不是有效的 IO 程序。当您尝试执行程序时

x <|> _whatever

你必须执行x看它是否抛出错误。但是,你 不能 执行 x,因为它不是程序——这是一个错误。相反,一切都爆炸了。

这与

有很大不同
x = throwIO _whatever

现在 x 是一个有效的程序。这是一个总是碰巧抛出错误的有效程序,但它仍然是一个可以实际执行的有效程序。当你尝试执行

x <|> _whatever

现在,x 被执行,产生的错误被丢弃,_whatever 被执行。您还可以认为计算 program/figuring 执行的内容与实际执行它之间存在差异。 throw 在计算要执行的程序时抛出错误("pure exception"),而 throwIO 在执行期间抛出错误("impure exception")。这也解释了它们的类型:throw returns任何类型因为所有类型都可以"computed",但是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"