F#: 选用 Some、None 还是 Exception?

15

最近我一直在自学F#,我的背景是命令式语言(C++/C#)。作为练习,我一直在做能够对矩阵进行加法、乘法、求行列式等操作的函数。在这方面我取得了很好的进展,但是我发现在处理无效输入时可能没有做出最佳决策,比如:

// I want to multiply two matrices
let mult m1 m2 =
  let sizeOK = validateDims m1 m2

  // Here is where I am running to conceptual trouble:
  // In a C# world, I would throw an exception.
  if !sizeOK then
    raise (InvalidOperationException("bad dimensions!")
  else
    doWork m1 m2  

虽然这个技巧在技术上可行,但是它是否适用于函数式编程呢? 它是否符合函数式编程的精神? 还是将其重写为以下形式更有意义:

所以,虽然这个技巧在技术上可行,但它是否符合函数式编程的原则呢?是否应该按照以下方式进行重写:

let mult m1 m2 =
  let sizeOK = validateDims m1 m2

  if !sizeOK then
    None
  else
    Some doWork m1 m2  

在这种情况下,我正在返回一个选项,它在矩阵周围添加了一个额外的层,但我也可以使用函数的结果,在程序的某个后续点中即使是失败案例(无)也可以使用模式匹配等。那么对于这些类型的场景是否有最佳实践?一个函数式编程人员会怎么做?


我认为后者更符合FP的“精神”。您可能还想了解一下Maybe单子,因为这是处理错误输入的常见方法之一。 - Onorio Catenacci
2
正如Pad在他的回答中所说,我也经常实现两个函数,一个在出错时抛出异常,另一个则返回一个Option/Choice而不是抛出异常。然后我使用让我的最终代码看起来最漂亮的那个函数 :) 为了更轻松地处理Option/Choice,您可能想阅读“铁路导向编程”:http://fsharpforfunandprofit.com/posts/recipe-part2/ - stmax
3个回答

10

出于以下原因,我倾向于避免使用异常:

  • .NET 异常速度较慢
  • 异常以意想不到的方式改变了程序的控制流,这使得推理更加困难
  • 在关键情况下常常会出现异常,而您可以通过使用选项来进行故障转移。

在您的情况下,我将遵循 F# 核心库的约定(例如 List.tryFindList.find 等),并创建两个版本:

let tryMult m1 m2 =
  let sizeOK = validateDims m1 m2

  if not sizeOK then
    None
  else
    Some <| doWork m1 m2

let mult m1 m2 =
  let sizeOK = validateDims m1 m2

  if not sizeOK then
    raise <| InvalidOperationException("bad dimensions!")
  else
    doWork m1 m2 

这个例子并不特别需要使用异常。`mult`函数是为了C#兼容性而包含的。在C#中使用你的库时,无法轻松地使用模式匹配来分解选项。

选项的一个缺点是它们不能说明函数未产生值的原因。在这里这样做有些过度;通常Choice(或Haskell术语中的Either单子)更适合错误处理:

let tryMult m1 m2 =
  // Assume that you need to validate input
  if not (validateInput m1) || not (validateInput m2) then
     Choice2Of2 <| ArgumentException("bad argument!")
  elif not <| validateDims m1 m2 then
    Choice2Of2 <| InvalidOperationException("bad dimensions!")
  else
    Choice1Of2 <| doWork m1 m2

很遗憾的是,F# Core缺乏操作Choice的高阶函数。你可以在FSharpXExtCore库中找到这些函数。


5
我喜欢以上的回答,但我想再加一种选择。这取决于结果有多意外和是否有意义继续进行。如果这是一个罕见事件,而调用者可能没有计划失败,那么异常就是完全可以接受的。捕获异常的代码可能会很多层级以上,而调用者可能没有计划失败。如果某个操作失败的结果是非常常见的,则Some/None也可以,虽然它只给出了两个选项,并且无法传递结果。另一种选择是制作区分联合,这将迫使调用者匹配不同的结果,具有可扩展性,并且不强制您使每个结果都成为相同的数据类型。
例如:
type MultOutcome =
    | RESULT of Matrix
    | DIMERROR 
    | FOOERROR of string


let mult a b =
    if dimensionsWrong then
        DIMERROR
    elif somethingElseIDoNotLike then
        FOOERROR("specific message")
    else
        DIMRESULT(a*b)


match mult x y with
    | DIMERROR ->  printfn "I guess I screwed up my matricies"
    | FOOERROR(s) -> printfn "Operation failed with message %s" s
    | DIMRESULT(r) ->
         // Proceed with result r

4
我倾向于遵循以下准则:
当函数本应始终返回值时,如果出现意外情况,请在函数中使用异常。例如,如果参数不符合函数的约定,则会出现这种情况。这样做的好处是客户端代码会更简单。
当函数有时针对有效输入返回值时,请使用Option。例如,在可能不存在有效密钥的映射上获取。这样可以强制用户检查函数是否有返回值。这可能会减少错误,但总是会使客户端代码变得混乱。
您的情况有些介于两者之间。如果您预计主要在有效维度的地方使用它,我会抛出异常。如果您期望客户端代码经常使用无效维度调用该函数, 我会返回一个Option。我可能会选择前者,因为它更清晰(见下文),但我不知道您的环境:
// With exception
let mult3 a b c = 
  mult (mult a b) c;

// With option
let mult3 a b c= 
   let option = mult a b
   match option with
     | Some(x) -> mult x b
     | None -> None

免责声明:我没有函数式编程的专业经验,但我是研究生阶段F#编程的助教。


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