14

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

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

4

3 回答 3

10

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

  • .NET 异常很慢
  • 异常以意想不到的方式改变程序的控制流,这使得推理变得更加困难
  • 在紧急情况下通常会出现异常,而您可以通过使用选项来确保故障安全。

在您的情况下,我将遵循 F# 核心库约定(例如List.tryFindandList.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 monad)更适合错误处理

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库中找到这些函数。

于 2013-08-27T18:50:18.050 回答
4

我倾向于遵循以下准则:

当出现意外错误时,在应该始终具有返回值的函数中使用异常。例如,如果参数不遵守函数的约定,则可能是这样。这样做的好处是客户端代码变得更简单。

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

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

// 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# 编程的研究生水平的 TA。

于 2013-08-27T16:01:59.710 回答
4

我喜欢上面的答案,但我想添加另一个选项。这实际上取决于结果有多出乎意料以及继续进行是否有意义。如果这是一个罕见的事件并且调用者可能不打算失败,那么例外是完全值得尊重的。捕获异常的代码可能在上面很多层,调用者可能不打算失败。如果操作失败是一个非常常规的结果,那么 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
于 2014-01-01T02:12:01.143 回答