“Lifting” 异常到 Option 类型

3

F#和Scala都是混合语言,通常用于将传统面向对象代码与函数式代码桥接。

属于面向对象世界的概念是异常,而函数式世界在许多情况下更喜欢Option类型。为了包装依赖异常的现有库代码并使其更加函数化,我想要将抛出异常的代码“提取”为返回Option类型。

在Scala中,有一个很好的库函数可以“捕获所有”并转换为Option。它可以像这样使用:

import scala.util.control.Exception._
val functionalVersion = allCatch opt myFunction

请参见在Scala中,是否存在将异常转换为选项的预定义库函数?

现在我正在转向F#,但我仍然有同样的要求,但似乎找不到现有的实用程序函数来完成这个任务,并且我也很难自己实现一个。

我可以为单位函数(即操作)创建这样的包装器。

let catchAll f = try Some (f()) with | _ -> None

但问题在于我不想首先将所有异常抛出的代码都包装成一个操作。
例如,我希望包装数组索引运算符,使其不会抛出异常。
// Wrap out-of-bounds exception with option type
let maybeGetIndex (array: int[]) (index: int) = catchAll (fun () -> array.[index])

maybeGetIndex [| 1; 2; 3 |] 10 // -> None

然而,如果能够简单地编写如下代码,那就更好了。
(catchAll a.[index])

即在表达式被求值之前将catchAll应用于整个表达式。 (Scala可以通过按名称调用参数实现这一点,但似乎在F#中缺少此功能)

因此,这个问题有两个方面:

  1. 是否存在现有的库函数将异常包装成选项类型?
  2. 是否有一种语言特性可以让我实现它?
1个回答

5
首先,我认为一个“更属于面向对象世界的概念是异常”并不正确。在许多ML家族的函数式语言中都存在异常,例如OCaml就相当依赖它们,甚至用于某些控制流结构。在F#中,情况并非如此,因为.NET异常有点慢,但我认为异常与面向对象/函数式问题非常正交。
因此,我实际上发现在F#中异常通常比选项类型更可取。缺点是有较少的类型检查(您不知道可能会抛出什么),但好处是语言提供了很好的集成异常支持。如果需要处理异常情况,那么异常是一个很好的方法!
回答你关于可以使用的语法技巧的原始问题 - 我可能会像你现有的代码一样使用函数,因为这是明确且易于理解的(并且假设您只需要在实现某些核心功能时包装异常)。
也就是说,您可以定义一个计算表达式生成器,将代码包装在主体中,并以稍微更整洁的语法作为您的catchAll函数:
type CatchAllBuilder() = 
  member x.Delay(f) = try Some(f()) with _ -> None
  member x.Return(v) = v

let catchAll = CatchAllBuilder()

这让你可以写出像这样的内容:
catchAll { return Array.empty.[0] }

正如之前所提到的,我不会这样做,因为(i)我认为在F#中并不需要将所有异常转换为选项,(ii)它引入了新团队成员(以及未来的你)可能会感到困惑的不熟悉语法,但这可能是你能得到的最好的语法。
[编辑:现在有一个带有return的可工作版本-这个版本略微没有那么好看,但或许仍然有用!]

2
此外,将异常转换为“None”会丢失可能有用的信息。将异常映射到“Result”更有意义,我认为。在Scala中,我更喜欢将异常映射到“Try”类型。 - Just another metaprogrammer
1
所有的观点都很好! 当然,异常与选项类型与OOP/FP范例是正交的。不过,在数组访问的例子中,我期望大多数“面向对象语言”在越界情况下会抛出异常,而“函数式语言”则可能提供一个选项类型版本。 当缺乏FP语言特性时,例如模式匹配,选项类型就不会那么有用了,因此在许多情况下,当OO语言依赖于异常时,选项是更好的选择。谢谢,回答得非常详细! - Gerold
我刚刚注意到您的构建器在非异常情况下无法按预期工作。catchAll {[| 1; 2; 3 |].[1] }会产生Some() - Gerold
@Gerold,你是对的!哎呀,我修正了代码片段 - 它需要使用return,但这使它有点不太美观。 - Tomas Petricek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接