有人可以解释一下为什么异常可能会在IO单子之外抛出,但只能在其中捕获吗?
其中一个原因是Haskell的指称语义。
(纯) Haskell函数的一个优良特性是它们的单调性 - 更明确定义的参数会产生更明确定义的值。这个特性非常重要,例如用于推理递归函数(阅读文章以了解其原因)。
异常的指称根据定义是底部_|_
,对应于给定类型的偏序集中的最小元素。因此,为满足单调性要求,任何Haskell函数的指称f
都需要满足以下不等式:
f(_|_) <= f(X)
现在,如果我们能够捕获异常,我们就可以通过“识别”底部(捕获异常)并返回更明确定义的值来打破这种不平等:
f x = case catch (seq x True) (\exception -> False) of
True -> -- there was no exception
undefined
False -> -- there was an exception, return defined value
42
这是一个完整的工作演示(需要base-4 Control.Exception):
import Prelude hiding (catch)
import System.IO.Unsafe (unsafePerformIO)
import qualified Control.Exception as E
catch :: a -> (E.SomeException -> a) -> a
catch x h = unsafePerformIO $ E.catch (return $! x) (return . h)
f x = case catch (seq x True) (\exception -> False) of
True -> -- there was no exception
undefined
False -> -- there was an exception, return defined value
42
==
意义上相等。)let x = x in x
这是一种非终止递归,因此它永远不会返回任何信息,并且被表示为|_|
。如果我们能够捕获异常,我们可以编写函数f,例如:
f undefined = 0
f (let x = x in x) = _|_
对于严格函数来说,后者总是成立的,因为Haskell没有检测非终止计算的手段 -- 而且原则上也不能,因为存在停机问题。
因为异常会破坏引用透明性。
你可能在谈论那些实际上是由输入直接导致的异常。例如:
head [] = error "oh no!" -- this type of exception
head (x:xs) = x
我可能在解释上有误,但这就是我的理解。
Haskell中的函数是纯函数,编译器有权以任何顺序对它们进行评估,仍然会产生相同的结果。例如,给定以下函数:
square :: Int -> Int
square x = x * x
表达式square (square 2)
可以用不同的方式进行求值,但它总是简化为相同的结果,即16。
如果我们从其他地方调用square
:
test x = if x == 2
then square x
else 0
square x
可以在实际需要值的时候,“外部”test
函数后进行评估。此时,调用堆栈可能与您在Java中期望的不同。square
抛出的潜在异常,catch
部分应该放在哪里呢?