Jul*_*pek 7 ocaml language-design
我已经阅读了/sf/answers/851336251/,它在性能方面解决了OCaml异常,并提到有人可能会使用异常来故意操纵控制流。
但是,我想从语言设计/历史角度了解为具有一流的Sum Types的语言添加例外的背后原理。
我的理解(如果我弄错了,请纠正我)是,OCaml中的异常会破坏类型系统,从而使推理程序的特定状态变得更加困难。与对总和类型进行匹配不同,编译器将不会检查是否处理了所有可能的错误情况,尤其是如果对库函数的修改引入了新的错误状态时,这可能会成为问题。例如,这就是为什么Zig编程语言强制执行错误处理并提供用于检查所有可能的错误情况的编译器强制构造(https://ziglang.org/#A-fresh-take-on-error-handling)。
鉴于以上所述,并且考虑到可能存在绕过多个堆栈框架的情况,我可以想象一个不同的语言构造(也许类似于标记中断),其角色与错误处理没有语义关联。 。
是否有(很多?)情况下处理错误的异常优于显式的,经过编译器检查的错误处理?
我尤其不理解Hashtbl.find抛出异常之类的东西。鉴于Hashtbl.find_opt几年前已经引入的方法,这是否表示在不破坏现有程序的情况下向标准库设计方向的某种转变?
OCaml和标准库中的异常是否是设计OCaml时的产物(例如,当时流行的异常/未完全理解其后果),和/或该语言是否有理由存在异常?
在很多情况下,无论是在可读性还是效率方面,异常都远远优于求和类型。是的,有时使用异常更安全。
我能想到的最好的例子也是最简单的:/如果它返回一个 sum 类型,那就太痛苦了。一段简单的代码,例如:
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 *)
Run Code Online (Sandbox Code Playgroud)
当然,这是一个极端的情况,错误 monad 可以使这变得更容易(事实上 monad 在 OCaml 中更有用),但这也表明了 sum 类型如何导致效率降低。顺便说一句,这种异常确实比 sum 类型更安全。代码可读性是一个极其重要的安全问题。
在很多情况下,您知道不会返回异常,即使它可以(循环内的数组访问for、除以您知道不为零的数字等)。大多数时候,编译器会注意到不会发生任何错误,并且可以删除死代码,但并非总是如此。当发生这种情况时,引发异常的代码将比基于和类型的代码更轻。
assert或 aprintf需要您更改函数签名除了这个标题我没什么可说的了。在代码中添加一些调试指令将需要您对其进行更改。这可能是您想要的,但它会完全破坏我的个人工作流程以及我认识的许多开发人员的工作流程。
保留这些例外的最后一个原因是向后兼容性。很多代码都依赖于Hashtbl.find. 在 OCaml 中重构很容易,但我们讨论的是全面的生态系统检修,会引入潜在的错误并造成一定的效率损失。
TL; 博士; 主要原因是性能。第二个原因是可用性。
将值包装到选项时间(或result类型)中需要分配并有其运行时成本。基本上,如果您有一个函数返回 anint并Not_found在未找到任何内容时引发,那么将此函数更改为int option将分配一个Some x值,这将创建一个在您的堆中占据两个字的装箱值。这与使用异常的版本中的零分配相比。将其置于紧密循环中会大大降低整体性能。像 10 到 100 次,真的。
即使返回值已经被装箱,它仍然会引入一个额外的框(一个字的开销)和一层间接。
在非整体世界中,非整体性具有传染性并在您的所有代码中传播,这一点很快就会变得非常明显。即,如果您的函数具有除法运算并且您没有例外来掩盖这一事实,那么您必须向前传播非总体性。很快,您将获得所有具有 的函数,('a,'b) result并且您将使用Resultmonad 使您的代码易于管理。但是 Result Monad 只不过是异常的具体化,只是更慢更笨拙。因此,我们又回到了现状。
显然,是的。一个例外是计算的副作用的特殊情况。OCaml 多核团队目前正致力于以Eff编程语言的风格向 OCaml 添加一个效果系统。这是一个演讲,我也找到了一些幻灯片。这个想法是你可以拥有两个世界的好处 - 有效函数的显式类型注释(如变体)和具有跳过无趣效果的高效表示(如例外)。
虽然我们这些普通人正在等待将效果交付给 OCaml,但我们仍然必须忍受异常和变体。那么我们应该怎么做呢?以下是我在 OCaml 中编程时采用的个人行为准则。
为了处理可用性问题,我采用了规则——对错误和程序员错误使用异常。更明确地说,如果一个函数有一个可检查的和明确定义的前提条件,那么它的错误用法就是程序员错误。如果程序损坏,则不应运行。因此,如果先决条件失败,则使用异常。一个很好的例子是Array.init函数,如果 size 参数为负,它就会失败。没有充分的理由使用resultsum 类型来告诉函数的用户,它没有正确使用它。这条规则的关键时刻是前提条件应该是可检查的——这意味着检查既快速又容易。即,主机存在或网络可达不是先决条件。
为了处理性能问题,我试图为每个非总函数提供两个接口,一个明确引发(应该在名称中说明),另一个使用结果类型作为返回值。后者通过前者实现。
例如,类似的东西,find_value_or_fail或(在使用 Janesteet 风格时,find_exn,而只是find.
此外,我一直在努力使我的功能健壮,基本上遵循 Internet 健壮性原则。或者,从逻辑的角度来看,使它们成为更强大的理论。换句话说,这意味着我试图最小化前提条件集,并为所有可能的输入提供合理的行为。例如,您可能会发现被零的急剧除法在模算术中具有明确定义的含义,在这种情况下,随着可分格满足并连接运算,GCD和 LCM 将开始变得有意义。
我们的世界可能比我们的理论更全面和完整,因为我们通常不会在我们周围看到很多异常:) 因此,在引发异常或以其他方式指示错误之前,请三思,它是错误还是只是你的理论不完整。