为什么 OCaml 使用异常而不是用 Sum Types 表示错误?
Why does OCaml use exceptions instead of representing errors with Sum Types?
我读过 ,它在某种程度上解决了性能上下文中的 OCaml 异常,并提到可以使用异常来故意操纵控制流。
但是,我想知道从语言 design/historical 的角度来看,向具有第一个 class 求和类型的语言添加异常背后的基本原理。
我的理解(如果我弄错了请纠正我)是 OCaml 中的异常颠覆了类型系统,从而使推断程序的特定状态变得更加困难。与求和类型的匹配不同,编译器不会检查是否处理了所有可能的错误情况,这可能会成为一个问题,特别是如果对库函数的修改引入了新的错误状态。这就是为什么,例如,Zig 编程语言强制执行错误处理并提供编译器强制构造来检查所有可能的错误情况 (https://ziglang.org/#A-fresh-take-on-error-handling)。
鉴于上述情况,并且考虑到可能存在绕过多个堆栈帧可能有用的情况,我可以想象一种不同的语言结构(可能类似于带标签的中断),其角色没有与错误处理的语义关联。
是否有任何(很多?)异常处理错误优于显式的、编译器检查的错误处理的情况?
我尤其不明白 Hashtbl.find
抛出异常。鉴于 Hashtbl.find_opt
是几年前推出的,这是否代表在不破坏现有程序的情况下标准库设计方向的一些转变?
OCaml 和标准库中的异常是否是 OCaml 设计时的产物(例如,异常在 time/were 流行,其后果尚未完全理解),and/or 是否有好的语言有例外的原因?
在很多情况下,异常在可读性和效率方面都远远优于求和类型。是的,有时使用异常更安全。
单独处理除以零将是地狱
我能想到的最好的例子也是最简单的:/
如果它返回一个总和类型,那将是一件很痛苦的事情。一段简单的代码,如:
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
循环中的数组访问,除以数字你知道不为零等)。大多数时候,编译器会注意到不会发生任何错误并且可以删除死代码,但并非总是如此。发生这种情况时,引发异常的代码将比基于 sum-type 的代码更轻。
添加 assert
或 printf
需要您更改函数签名
除了这个标题,我没什么好说的。在您的代码中添加一些调试指令将需要您对其进行更改。这可能是你想要的,但它会完全破坏我的个人工作流程以及我认识的许多开发人员的工作流程。
Retro-compatibility
保留这些例外的最终原因是 retro-compatibility。很多代码都依赖于 Hashtbl.find
。在 OCaml 中重构很容易,但我们正在谈论一个完整的生态系统检修,引入了潜在的错误和一定的效率损失。
TL;DR;主要原因是性能。第二个原因是可用性。
性能
将值包装到选项类型(或 result
类型)需要分配并有其 运行 时间成本。基本上,如果你有一个函数 returning 一个 int
并在没有找到任何东西的情况下引发 Not_found
,然后将此函数更改为 int option
将分配一个 Some x
值,这将在您的堆中创建一个占据两个单词的盒装值。这与使用异常的版本中的零分配相比。将其置于紧密循环中会大大降低整体性能。 10 到 100 次,真的。
即使returned 值已经装箱,它仍然会引入一个额外的箱子(一个单词的开销)和一层间接。
可用性
在 non-total 世界中,non-totality 具有传染性并会在您的所有代码中传播,这一点很快就会变得非常明显。也就是说,如果你的函数有一个除法运算并且你没有例外来掩盖这个事实,那么你必须向前传播 non-totality。很快,您将得到所有具有 ('a,'b) result
的函数,并且您将使用 Result
monad 使您的代码易于管理。但是 Result Monad 只不过是异常的具体化,只是更慢更笨拙。所以我们又回到了现状。
有没有理想的解决方案?
显然,是的。异常是计算副作用的特例。 OCaml Multicore 团队目前正致力于以 Eff programming language. Here is a talk and I've found some slides 的风格向 OCaml 添加效果系统。这个想法是您可以享受两个世界的好处 - 有效函数的显式类型注释(与变体一样)和具有跳过无趣效果的能力的有效表示(与异常一样)。
现在做什么?
当我们这些普通人在等待将效果交付给 OCaml 时,我们仍然不得不忍受异常和变体。那我们该怎么办呢?以下是我在 OCaml 编程时遵循的个人行为准则。
为了处理可用性问题,我采用了规则——对错误和程序员错误使用例外。更明确地说,如果一个函数有一个可检查和明确定义的前提条件,那么它的不正确使用就是程序员的错误。如果一个程序坏了,它不应该 运行。因此,如果先决条件失败,则使用异常。一个很好的例子是 Array.init
函数,如果 size 参数为负数,它就会失败。没有充分的理由使用 result
求和类型来告诉函数的用户,它使用不正确。这条规则的关键时刻是先决条件应该是可检查的——这意味着检查既快又容易。即,host-exists 或 network-is-reachable 不是先决条件。
为了处理性能问题,我试图为每个 non-total 函数提供两个接口,一个明确提出(应在名称中说明),另一个使用结果类型作为 return 值。后者通过前者实现。
例如,find_value_or_fail
或(在使用 Janesteet 样式时,find_exn
,以及 find
。
此外,我一直在努力使我的功能健壮,基本上遵循 Internet 健壮性原则。或者,从逻辑的角度,使它们成为更强的理论。换句话说,这意味着我正在尝试最小化前提条件集并为所有可能的输入提供合理的行为。例如,您可能会发现被零彻底除法有一个 well-defined meaning in the modular arithmetics, under which GCD,并且 LCM 将开始有意义,因为可分性格会合并连接操作。
我们的世界可能比我们的理论更全面、更完整,因为我们周围通常看不到很多异常 :) 因此,在引发异常或以其他方式指示错误之前,请三思,它是不是错误或者这只是您的理论不完整。
我读过 ,它在某种程度上解决了性能上下文中的 OCaml 异常,并提到可以使用异常来故意操纵控制流。
但是,我想知道从语言 design/historical 的角度来看,向具有第一个 class 求和类型的语言添加异常背后的基本原理。
我的理解(如果我弄错了请纠正我)是 OCaml 中的异常颠覆了类型系统,从而使推断程序的特定状态变得更加困难。与求和类型的匹配不同,编译器不会检查是否处理了所有可能的错误情况,这可能会成为一个问题,特别是如果对库函数的修改引入了新的错误状态。这就是为什么,例如,Zig 编程语言强制执行错误处理并提供编译器强制构造来检查所有可能的错误情况 (https://ziglang.org/#A-fresh-take-on-error-handling)。
鉴于上述情况,并且考虑到可能存在绕过多个堆栈帧可能有用的情况,我可以想象一种不同的语言结构(可能类似于带标签的中断),其角色没有与错误处理的语义关联。
是否有任何(很多?)异常处理错误优于显式的、编译器检查的错误处理的情况?
我尤其不明白 Hashtbl.find
抛出异常。鉴于 Hashtbl.find_opt
是几年前推出的,这是否代表在不破坏现有程序的情况下标准库设计方向的一些转变?
OCaml 和标准库中的异常是否是 OCaml 设计时的产物(例如,异常在 time/were 流行,其后果尚未完全理解),and/or 是否有好的语言有例外的原因?
在很多情况下,异常在可读性和效率方面都远远优于求和类型。是的,有时使用异常更安全。
单独处理除以零将是地狱
我能想到的最好的例子也是最简单的:/
如果它返回一个总和类型,那将是一件很痛苦的事情。一段简单的代码,如:
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
循环中的数组访问,除以数字你知道不为零等)。大多数时候,编译器会注意到不会发生任何错误并且可以删除死代码,但并非总是如此。发生这种情况时,引发异常的代码将比基于 sum-type 的代码更轻。
添加 assert
或 printf
需要您更改函数签名
除了这个标题,我没什么好说的。在您的代码中添加一些调试指令将需要您对其进行更改。这可能是你想要的,但它会完全破坏我的个人工作流程以及我认识的许多开发人员的工作流程。
Retro-compatibility
保留这些例外的最终原因是 retro-compatibility。很多代码都依赖于 Hashtbl.find
。在 OCaml 中重构很容易,但我们正在谈论一个完整的生态系统检修,引入了潜在的错误和一定的效率损失。
TL;DR;主要原因是性能。第二个原因是可用性。
性能
将值包装到选项类型(或 result
类型)需要分配并有其 运行 时间成本。基本上,如果你有一个函数 returning 一个 int
并在没有找到任何东西的情况下引发 Not_found
,然后将此函数更改为 int option
将分配一个 Some x
值,这将在您的堆中创建一个占据两个单词的盒装值。这与使用异常的版本中的零分配相比。将其置于紧密循环中会大大降低整体性能。 10 到 100 次,真的。
即使returned 值已经装箱,它仍然会引入一个额外的箱子(一个单词的开销)和一层间接。
可用性
在 non-total 世界中,non-totality 具有传染性并会在您的所有代码中传播,这一点很快就会变得非常明显。也就是说,如果你的函数有一个除法运算并且你没有例外来掩盖这个事实,那么你必须向前传播 non-totality。很快,您将得到所有具有 ('a,'b) result
的函数,并且您将使用 Result
monad 使您的代码易于管理。但是 Result Monad 只不过是异常的具体化,只是更慢更笨拙。所以我们又回到了现状。
有没有理想的解决方案?
显然,是的。异常是计算副作用的特例。 OCaml Multicore 团队目前正致力于以 Eff programming language. Here is a talk and I've found some slides 的风格向 OCaml 添加效果系统。这个想法是您可以享受两个世界的好处 - 有效函数的显式类型注释(与变体一样)和具有跳过无趣效果的能力的有效表示(与异常一样)。
现在做什么?
当我们这些普通人在等待将效果交付给 OCaml 时,我们仍然不得不忍受异常和变体。那我们该怎么办呢?以下是我在 OCaml 编程时遵循的个人行为准则。
为了处理可用性问题,我采用了规则——对错误和程序员错误使用例外。更明确地说,如果一个函数有一个可检查和明确定义的前提条件,那么它的不正确使用就是程序员的错误。如果一个程序坏了,它不应该 运行。因此,如果先决条件失败,则使用异常。一个很好的例子是 Array.init
函数,如果 size 参数为负数,它就会失败。没有充分的理由使用 result
求和类型来告诉函数的用户,它使用不正确。这条规则的关键时刻是先决条件应该是可检查的——这意味着检查既快又容易。即,host-exists 或 network-is-reachable 不是先决条件。
为了处理性能问题,我试图为每个 non-total 函数提供两个接口,一个明确提出(应在名称中说明),另一个使用结果类型作为 return 值。后者通过前者实现。
例如,find_value_or_fail
或(在使用 Janesteet 样式时,find_exn
,以及 find
。
此外,我一直在努力使我的功能健壮,基本上遵循 Internet 健壮性原则。或者,从逻辑的角度,使它们成为更强的理论。换句话说,这意味着我正在尝试最小化前提条件集并为所有可能的输入提供合理的行为。例如,您可能会发现被零彻底除法有一个 well-defined meaning in the modular arithmetics, under which GCD,并且 LCM 将开始有意义,因为可分性格会合并连接操作。
我们的世界可能比我们的理论更全面、更完整,因为我们周围通常看不到很多异常 :) 因此,在引发异常或以其他方式指示错误之前,请三思,它是不是错误或者这只是您的理论不完整。