【问题标题】:Why is there difference between throw and throwIO?为什么 throw 和 throwIO 有区别?
【发布时间】:2019-12-15 12:17:37
【问题描述】:

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

【问题讨论】:

  • 但是写这篇文章的人对“微妙”的含义有奇怪的想法。
  • 这对我来说太微妙了,我不明白其中的区别。我的代码在哪里是seq
  • 对这种循环使用AlternativeIO 实例是不习惯的。在 Java 中,这就像在 for 循环体的末尾抛出异常以推进下一次迭代。
  • @danidiaz 那么,Alternative IO 的惯用语是什么?

标签: haskell exception io-monad alternative-functor


【解决方案1】:

说你有

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"

【讨论】:

  • 最后一段有点难。
  • @IgnatInsarov 更好吗?
  • 是的,好多了。我有点困惑,因为那里有another definition of impure exceptions,我无法将我在那里读到的内容与你所说的保持一致。
【解决方案2】:

throwundefinederror 的泛化,是为了在纯代码中抛出异常。当异常的值无关紧要时(大多数情况下),它用符号 ⟘ 表示“未定义值”。

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

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

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

问题是(&lt;|&gt;) 被定义为mplusIO,它使用catchException,这是catch 的严格变体。这种严格性总结如下:

⟘ <|> x = ⟘

因此您在throw 变体中得到一个异常(并且x 永远不会运行)。

请注意,如果不严格,“未定义的动作”(即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

【讨论】:

  • @IgnatInsarov throwIO 的文档指出,在 IO 上下文中工作时,它应该优先于 throw
  • 如果有的话throw 是可憎的。如果您有异常抛出它们是一个非常好的操作,那就是throwIO
  • @danidiaz 它是否也解释了原因?我已经看过盒子上的内容了——也许是两次——但我仍然没有看到图片。
  • @Li-yaoXia 但我也可以捕获thrown 异常。无论如何,我们在纯计算中都会发生异常,不是吗?比如ArithException
  • 但我们也无法真正摆脱throw(或errorundefined)。在具有一般递归(Haskell)的非严格语言中,至少由于非终止,您将具有“未定义的值”,因此您最好保留抛出异常的能力而不是进入无限循环并充分利用它。
【解决方案3】:

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


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

对于throwIO&lt;|&gt; 不评估任何内容;它正在创建一个新的IO 动作,当它确实被执行时,将首先查看它的第一个参数。运行时可以“安全地”执行IO 操作并看到它实际上没有提供值,此时它可以停止并尝试&lt;|&gt; 表达式的另一“半”。

【讨论】:

  • 我喜欢将 Haskell 视为一个纯函数式 DSL(没有例外),用于编写 IO 程序,这些程序是用一种您几乎无法直接访问的语言编写的。李耀说得很好:throw 让您可以直接从纯 Haskell 代码中引发 IO 解释器内部的异常。
猜你喜欢
  • 2012-02-27
  • 1970-01-01
  • 2020-12-04
  • 2018-02-27
  • 2020-12-20
  • 1970-01-01
  • 2010-12-01
相关资源
最近更新 更多