5

我试图牢牢掌握异常,以便改进我的条件循环实现。为此,我进行了各种实验,扔东西,看看会被抓到什么。

这个让我惊喜不已:

% 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 内部模块;虽然我自己或多或少可以理解不同代码片段的含义,但我很难看到整个画面。

如果很难预测,甚至应该使用这个替代实例吗?如果它消除任何同步异常,而不仅仅是以特定方式定义并以特定方式抛出的一小部分异常,这不是更好吗?

4

3 回答 3

6

throw是 and 的概括undefinederror它意味着在纯代码中抛出异常。当异常的值无关紧要时(大多数情况下),它用符号 ⟘ 表示“未定义值”。

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

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

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

问题在于,它(<|>)被定义为mplusIOwhich uses catchExceptionwhich 是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
于 2019-08-07T20:21:30.913 回答
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"
于 2019-08-07T21:08:59.840 回答
3

这个答案的其余部分可能并不完全正确。但从根本上说,区别在于:throwIO终止并返回一个IO动作,而同时throw终止。


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

使用throwIO,<|>不评估任何东西;它正在创建一个新IO动作,当它执行时,将首先查看它的第一个参数。运行时可以“安全地”执行该IO操作,并看到它实际上没有提供值,此时它可以停止并尝试<|>表达式的另一“半”。

于 2019-08-07T20:11:04.777 回答