由于非确定性异常的主要来源是IO,并且您只能在IO monad中捕获异常,因此它接合合理,不会从纯函数中抛出异常。
究竟在纯粹的功能中会出现什么样的“特殊”?空列表或除零并不是特殊的,可以预期。那么为什么不仅使用Maybe
,Either
或[]
来代表纯代码中的此类案例。
有许多纯函数,例如(!!)
,tail
,div
,它们会抛出异常。使它们不安全的原因是什么?
答案 0 :(得分:6)
不安全的函数都是部分函数的例子;它们没有为其域中的每个值定义。考虑head :: [a] -> a
。其域名为[a]
,但head
未定义[]
:没有类型a
的值可以返回正确。 safeHead :: [a] -> Maybe a
之类的内容是 total 函数,因为您可以为任何列表返回有效的Maybe a
; safeHead [] = Nothing
和safeHead (x:xs) = Just x
。
理想情况下,您的程序仅包含总功能,但在实践中并非总是可行。 (可能有太多未定义的值需要预测,或者您无法提前知道哪些值会导致问题。)异常显然表明您的程序没有很好地定义。当您收到异常时,表示您需要将代码更改为
在任何情况下都不应该“3.继续以未定义的值运行代替函数的返回值”被视为可接受。
(有些猜想可以遵循,但我认为它大多是正确的。)从历史上看,Haskell没有一种处理异常的好方法。在调用head :: [a] -> a
之前检查列表是否为空可能比处理Maybe a
之类的返回值更容易。一旦引入monad,这就成了一个问题,它提供了一个通用框架,用于将safeHead :: [a] -> Maybe a
的输出提供给a -> b
类型的函数。鉴于很容易识别head []
未定义,提供有用的特定错误消息至少比依赖于通用错误消息更简单。现在像safeHead
这样的函数更容易使用,head
之类的函数可以被视为历史遗迹而不是模拟的模型。
答案 1 :(得分:4)
有时候,关于程序行为的 true 的东西在其源语言中不是 provable 。其他时候,它可能是可证明的,但不是有效所以。还有一些时候,它可能是可证明的,但证明它需要程序员花费大量的时间和精力。
Data.Sequence
将序列表示为带有大小注释的指纹树。它保持不变量,即任何子树中的元素数等于存储在其根中的注释。序列的zipWith
的实现将较长的序列拆分为与较短序列的长度相匹配,然后使用有效的,操作上懒惰的技术将它们压缩在一起。
该技术涉及沿着第一序列的自然结构多次分裂第二序列。当它到达第一个序列的叶子时,它依赖于具有恰好一个元素的第二序列的相关片段。只要保持注释不变量,就可以保证这种情况发生。如果此不变量失败,zipWith
别无选择,只能抛出错误。
要在Haskell中编码注释不变量,您需要使用它们的长度索引手指树的基础部分。然后,您需要每个操作证明它维护不变量。这种事情是可能的,像Coq,Agda和Idris这样的语言试图减少痛苦和低效率。但他们仍然有痛苦,有时甚至是效率低下。 Haskell还没有真正适合这样的工作,并且可能永远不会很好(它不是它作为一种语言的主要目标)。这将是非常痛苦的,也是非常低效的。由于效率是首先选择此实施的原因,因此这不是一种选择。
答案 2 :(得分:3)
某些函数具有与之关联的前提条件(!!
需要有效索引,tail
需要非空列表,div
需要非零除数。违反前提条件会导致异常,因为您没有遵守合同。
另一种方法是不使用前置条件,而是使用一个返回值来指示调用是否成功。
这些都是核心功能,因此它们需要易于使用,这是支持异常前置条件的重点。它们也是纯粹的,所以当它们失败时永远不会出现意外:你确切知道何时会发生这种情况,即当你传递违反先决条件的论据时。但是,最终,它归结为一种设计选择,有利于并反对两种解决方案。