为什么OCaml使用异常而不是用求和类型表示错误?

时间:2019-07-11 04:48:50

标签: ocaml language-design

我已经读过https://stackoverflow.com/a/12161946/,它在性能方面解决了OCaml异常,并提到有人可能会使用异常来故意操纵控制流。

但是,我想从语言设计/历史角度了解为具有一流的Sum Types的语言添加例外的原理。

我的理解(如果我弄错了,请纠正我)是OCaml中的异常会颠覆类型系统,从而使推理程序的特定状态变得更加困难。与对总和类型进行匹配不同,编译器将不会检查是否处理了所有可能的错误情况,这可能会成为问题,特别是如果对库函数的修改引入了新的错误状态。例如,这就是为什么Zig编程语言会强制执行错误处理并提供用于检查所有可能的错误情况(https://ziglang.org/#A-fresh-take-on-error-handling)的编译器强制构造的原因。

鉴于上述情况,并且考虑到可能存在绕过多个堆栈框架的情况,我可以想象一个不具有语义关联的角色的不同语言构造(也许类似于标记中断)错误处理。

是否有很多情况下处理错误的异常优于显式的,经过编译器检查的错误处理?

我尤其不理解类似Hashtbl.find引发异常的事情。鉴于Hashtbl.find_opt是在几年前引入的,这是否表示在不破坏现有程序的情况下向标准库设计方向的转变?

OCaml和标准库中的异常是否是OCaml设计时的产物(例如,当时流行的异常/未完全理解其后果),和/或是否有充分的理由使该语言具有例外?

2 个答案:

答案 0 :(得分:4)

TL; DR;主要原因是性能。第二个原因是可用性。

性能

将值包装为选项时间(或result类型)需要分配并具有其运行时成本。基本上,如果您有一个函数返回int并在没有找到任何内容的情况下提高Not_found,则将此函数更改为int option会分配一个Some x值,这将创建一个装箱的值在您的堆中占用两个单词。与使用异常的版本中的零分配相比。使其处于紧密循环中会大大降低整体性能。大概是10到100次。

即使返回值已经装箱,它仍然会引入一个额外的框(一个字的开销)和一层间接寻址。

可用性

在非完全的世界中,很明显非完全具有传染性,并蔓延到所有代码中。也就是说,如果您的函数具有除法运算,并且您没有例外来掩盖此事实,那么您必须向前传播非总计。很快,您将获得所有带有('a,'b) result的函数,并且将使用Result monad使代码易于管理。但是Result Monad只是对例外的重新定义,只是速度较慢且比较尴尬。因此,我们回到了现状。

有理想的解决方案吗?

显然,是的。例外是计算的副作用的特殊情况。 OCaml多核团队目前正在以Eff编程语言的样式向OCaml添加效果系统。这是talk,我也发现了一些slides。这个想法是,您可以同时拥有两个世界的好处-有效功能的显式类型注释(如变体)和具有跳过无用效果的能力的有效表示(如例外)。

现在该怎么办?

虽然我们这些普通人正在等待将效果传递给OCaml,但我们仍然必须忍受异常和变体。那我们该怎么办呢?以下是我在OCaml编程时使用的个人行为准则。

要处理可用性问题,我采用了规则-对bug和程序员错误使用异常。更明确地说,如果一个函数具有可检查且明确定义的前提条件,则其不正确的用法是程序员错误。如果程序损坏,则不应运行。因此,如果前提条件失败,请使用异常。 Array.init函数就是一个很好的例子,如果size参数为负,该函数将失败。没有充分的理由使用result sum类型来告知用户该函数使用不正确。该规则的关键时刻是前提条件应该是可检查的-这意味着该检查既快速又容易。即,主机存在或网络可达不是先决条件。

为处理性能问题,我试图为每个非总计函数提供两个接口,一个接口明确提出(应在名称中说明),另一个使用结果类型作为返回值。后者是通过前者实现的。

例如,find_value_or_fail或(使用Janesteet样式,find_exn,仅使用find

此外,我一直在努力通过基本遵循Internet健壮性原则来使我的功能健壮。或者,从逻辑角度来看,使它们成为更强的理论。换句话说,这意味着我正在尝试最小化前提条件,并为所有可能的输入提供合理的行为。例如,您可能会发现在模块算术中,以零为单位的急剧除法具有well-defined meaning,在这种情况下,GCD和LCM将随着可除性格的满足和结合运算而变得有意义。

我们的世界可能比我们的理论更全面和完整,因为我们通常不会在我们周围看到很多异常:)因此,在引发异常或以其他方式指示错误之前,请三思而后行,是错误还是这只是您理论的不完整。

答案 1 :(得分:3)

在可读性和效率方面,有很多情况下异常远远优于求和类型。是的,使用异常有时更安全。

仅处理被零除将是地狱

我能想到的最好的示例也是最简单的示例:/如果返回总和类型,将是一种痛苦。一段简单的代码如下:

let x = ( 4 / 2 ) + ( 3 / 2 ) (* OCaml code *)
let x' = match ( 4 / 2 ), ( 3 / 2 ) with
         | Some a, Some b -> Some ( a + b )
         | None, _ | _, None -> None (* Necessary code because of division by zero *)

当然,这是一个极端的情况,错误的monad会使错误变得更容易(并且monad实际上将在OCaml中更加可用),但这也显示了求和类型如何导致效率降低。顺便说一句,这种方式确实可以比总和类型更安全 。代码的可读性是一个非常重要的安全问题。

这会创建很多无效代码

在很多情况下,您知道,即使可能返回异常也不会返回(for循环中的数组访问,除以您不知道的数字零等)。在大多数情况下,编译器会注意到不会发生任何错误,并且可以删除无效代码,但并非总是如此。发生这种情况时,引发异常的代码将比基于和类型的代码轻。

添加assertprintf将要求您更改函数签名

除了这个标题,我无话可说。在代码中添加一些调试指令将需要您对其进行更改。那可能就是您想要的,但这将完全破坏我的个人工作流程以及我认识的许多开发人员的工作流程。

复古兼容性

保留这些例外的最终原因是追溯兼容性。那里的许多代码都依赖于Hashtbl.find。在OCaml中,重构很容易,但是我们正在谈论的是对生态系统的全面检查,其中引入了潜在的错误,并且一定程度上降低了效率。