F#:一些,没有,还是例外?

F#:一些,没有,还是例外?,f#,c#-to-f#,f#-3.0,F#,C# To F#,F# 3.0,我最近一直在自学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:

我最近一直在自学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  

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

我倾向于遵循以下准则:

在假定总是有返回值的函数中使用异常,当出现意外错误时。例如,如果参数不符合函数的约定,则可能出现这种情况。这样做的好处是客户端代码变得更简单

当函数有时具有有效输入的返回值时,请使用选项。例如,这可能是在可能不存在有效密钥的地图上获取的。因此,您强制用户检查函数是否有返回值。这可能会减少bug,但总是会弄乱客户端代码

你的情况介于两者之间。如果您希望它主要用于尺寸有效的地方,我会抛出一个异常。 如果您希望客户端代码经常使用无效维度调用它,我将返回一个选项。我可能会选择前者,因为它更干净(见下文),但我不知道您的背景:

// 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#编程助教

我倾向于避免例外,原因如下:

  • 异常以一种意想不到的方式改变了程序的控制流,这使得推理变得更加困难
  • 异常通常出现在关键情况下,而您可以使用选项进行故障保护
在您的情况下,我将遵循F#核心库约定(例如
List.tryFind
List.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 

遗憾的是,F#Core缺乏高阶函数来操纵选择。您可以在或库中找到这些函数。

我喜欢上面的答案,但我想添加另一个选项。这实际上取决于结果有多出乎意料,以及继续进行是否有意义。如果这是一个罕见的事件,并且调用方可能没有计划失败,那么异常是完全值得尊敬的。捕获异常的代码可能在上面的许多级别,调用方可能没有计划失败。如果操作失败是一个非常常规的结果,那么Some/None是可以的,尽管它只提供了两个选项,并且没有传递结果的方法。另一种选择是将各种可能性结合起来。这迫使调用者可能匹配不同的结果,是可扩展的,并且不会强迫您使每个结果都具有相同的数据类型

e、 g


我认为后者更符合FP的“精神”。您可能需要研究Maybe monad,这是处理错误输入的一种常见方法。正如pad在回答中所说,我还经常实现两个函数,一个是抛出错误的函数,另一个是返回选项/选择而不是抛出的相同try函数。然后我使用使我的最终代码看起来最漂亮的代码:)为了更容易地处理选项/选择,您可能需要阅读“面向铁路的编程”:
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
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