在尝试用 Haskell 编写程序时,我突然意识到我显然不明白错误抛出/捕获异常是如何工作的。虽然我的实际情况要复杂得多,但我想出了一个看似最小的例子,显示了我不明白的内容:
import Control.Exception
import Control.Monad
import Data.Typeable
data IsFalse = IsFalse
deriving (Show, Typeable)
instance Exception IsFalse
isTrue :: Bool -> Bool
isTrue b = if b then b else throw IsFalse
catchesFalse :: Bool -> IO ()
catchesFalse = try . return . isTrue >=> either (\e -> fail $ displayException (e :: IsFalse)) (const $ putStrLn "uh-oh")
main :: IO ()
main = catchesFalse False
Run Code Online (Sandbox Code Playgroud)
运行时runhaskell
,我希望上面的代码失败并打印IsFalse
。但是,它会打印uh-oh
. 在另一方面,如果我取代的定义catchesFalse
由
catchesFalse = try . return . isTrue >=> either (\e -> fail $ displayException (e :: IsFalse)) print
Run Code Online (Sandbox Code Playgroud)
然后异常被捕获,正如我所期望的那样。
我希望有人可以向我指出任何可以帮助我理解这两个功能之间差异的资源。我最好的猜测是懒惰评估发生了一些事情,但我不确定。
如果情况确实如此,那么强制 Haskell 将表达式求值到可以捕获异常的程度的最佳方法是什么?原谅我,我知道这个特定的问题可能有很多答案,这取决于我真正想评估的内容(在我的实际情况中,这远没有 那么简单Bool
)。
你可能想要的是evaluate
:
catchesFalse = try . evaluate . isTrue >=> either (\e -> fail $ displayException (e :: IsFalse)) (const $ putStrLn "uh-oh")
Run Code Online (Sandbox Code Playgroud)
有了这个定义,catchesFalse False
将导致
*** Exception: user error (IsFalse)
Run Code Online (Sandbox Code Playgroud)
请注意,user error
这里暗示这实际上是由fail
.
您的两个示例都没有“捕获”异常。第二个通过调用触发它print
。
“纯”(即非 IO)计算中的例外是棘手的。事实上,我们有以下等式
*** Exception: user error (IsFalse)
Run Code Online (Sandbox Code Playgroud)
让我们看看第一个等式,它可能更令人惊讶。该函数try
是根据 实现的catch
,并catch
包装给定的 IO 计算并检查其执行中是否有任何影响。然而,执行并不意味着评估,它只涉及计算的“有效”部分。Areturn
是一个微不足道的 IO 计算,它会立即“成功”。无论结果如何,都catch
不会也不会try
对此采取行动。
第二个方程简单地遵循单子定律。
如果我们牢记这一点,并将等式推理应用于您的示例,我们会得到第一种情况:
try (return e) >>= f
=
return (Right e) >>= f
=
f (Right e)
Run Code Online (Sandbox Code Playgroud)
如您所见,异常甚至从未被触发。
在第二个例子中,一切都一样,直到快结束,我们得到
catchesFalse False
=
(try . return . isTrue >=> either (\ e -> fail $ displayException (e :: IsFalse)) (const $ putStrLn "uh-oh")) False
=
try (return (isTrue False)) >>= either (\ e -> fail $ displayException (e :: IsFalse)) (const $ putStrLn "uh-oh")
=
return (Right (isTrue False)) >>= either (\ e -> fail $ displayException (e :: IsFalse)) (const $ putStrLn "uh-oh")
=
either (\ e -> fail $ displayException (e :: IsFalse)) (const $ putStrLn "uh-oh") (Right (isTrue False))
=
(const $ putStrLn "uh-oh") (isTrue False)
=
putStrLn "uh-oh"
Run Code Online (Sandbox Code Playgroud)
现在,当执行这个时,print
将强制它的参数,从而触发异常,这将产生输出:
*** Exception: IsFalse
Run Code Online (Sandbox Code Playgroud)
这直接来自throw
,而不是来自您的处理程序;user error
输出中没有。
evaluate
在返回 IO 操作时使用this 进行了更改,该操作在“返回”之前强制其参数为弱头范式,从而将在参数表达式评估期间出现的一定数量的异常提升为可以在结果执行期间捕获的异常IO 行动。
但是请注意,这evaluate
并没有完全评估其参数,而只是对弱头范式(即最外层构造函数)进行评估。
总而言之,这里需要非常小心。通常,建议避免“纯”代码中的异常,并使用明确允许失败的类型(例如Maybe
和变体)来代替。