管道序列中的异常处理

时间:2011-11-23 22:09:08

标签: f#

我正在研究基本的2D CAD引擎,管道运算符显着改进了我的代码。基本上,几个函数以空间中的点(x,y)开始,并在多次移动操作后计算最终位置:

let finalPosition =
    startingPosition
    |> moveByLengthAndAngle x1 a1 
    |> moveByXandY x2 y2
    |> moveByXandAngle x3 a3
    |> moveByLengthAndAngle x4 a4
    // etc...

这非常容易阅读,我想保持这种方式。各种x1,a1等在实际代码中显然有一个含义名称。

现在新要求是引入异常处理。围绕整个操作链进行一次大尝试是不够的,因为我想知道哪一行引起了异常。我需要知道哪个参数无效,以便用户知道必须更改哪个参数。

例如,如果第一行(moveByLengthAndAngle x1 a1)引发异常,我想告诉类似“嘿,-90是a1的无效值!a1必须在45到90之间!”。鉴于可以在序列中使用相同类型的许多操作,仅为每个操作定义不同的异常类型是不够的(在该示例中,我将无法判断错误是第一次还是最后一次移动)。

显而易见的解决方案是在单个let语句中拆分链,每个语句在各自的try / with中。然而,这将使我漂亮和可读的代码有点凌乱,不再那么可读。

有没有办法在不牺牲当前代码的可读性和优雅的情况下满足这一要求?

(注意。现在每个moveBy函数都会在出现错误的情况下引发异常,但我可以自由地更改ex。返回一个选项,一个更大的元组,或者只需要其他任何东西)。

5 个答案:

答案 0 :(得分:5)

有很多方法可以解决这个问题,最简单的方法是将每个调用包装在try-with块中:

let finalPosition =
    startingPosition
    |> (fun p -> try moveByLengthAndAngle x1 a1 p with ex -> failwith "failed moveByLengthAndAngle")
    |> (fun p -> try moveByXandY x2 y2 p with ex -> failwith "failed moveByXandY")
    |> (fun p -> try moveByXandAngle x3 a3 p with ex -> failwith "failed moveByXandAngle")
    |> (fun p -> try moveByLengthAndAngle x4 a4 p with ex -> failwith "failed moveByLengthAndAngle")
    // etc...

观看表达式编程的力量:)。

不幸的是,如果你在一个序列上进行流水线处理,那么它将变得更加困难:

  1. 管道中发生的事情(对于Seqs)是组合,而不是执行。
  2. IEnumerable中的异常处理是未定义的,因此取决于Enumerator的实现。
  3. 唯一安全的方法是确保包装每个序列操作的内部。

    编辑:哇,我简直不敢相信我搞砸了。它现在已经修复,但我确实认为其他两种解决方案更清晰。

答案 1 :(得分:5)

Rick描述的解决方案只是处理在评估管道中函数的参数时引发的异常。但是,它不会处理由流水线函数引发的异常(如in answer to your other question所述)。

例如,假设您有这些简单的功能:

let times2 n = n * 2
let plus a b = a + b
let fail n = failwith "inside fail"

10 // This will handle exception that happens when evaluating arguments
   |> try plus (failwith "evaluating args") with _ -> 0 
   |> times2                                            
   |> try fail with _ -> 0 // This will not handle the exception from 'fail'!

要解决此问题,您可以编写一个函数来包装异常处理程序中的任何其他函数。您的protect函数将采用函数(例如times2fail)的想法将返回一个新函数,该函数从管道(数字)获取输入并将其传递给function(times2fail),但会在异常处理程序中执行此操作:

let protect msg f = 
  fun n -> 
    try
      f n 
    with _ ->
      // Report error and return 0 to the pipeline (do something smarter here!)
      printfn "Error %s" msg
      0

现在,您可以保护管道中的每个函数,它还将处理评估这些函数时发生的异常:

let n =
  10 |> protect "Times" times2
     |> protect "Fail" fail
     |> protect "Plus" (plus 5)

答案 2 :(得分:5)

折叠选择怎么样?让我们说,不是流水线操作,而是代表它们:

let startingPosition = 0. ,0.

let moveByLengthAndAngle l a (x,y) = x,y // too lazy to do the math
let moveByXandY dx dy (x,y) = 
    //failwith "oops"
    x+dx, y+dy
let moveByXandAngle dx a (x,y) = x+dx, y

let actions = 
    [
        moveByLengthAndAngle 0. 0., "failed first moveByLengthAndAngle"
        moveByXandY 1. 2., "failed moveByXandY"
        moveByXandY 3. 4., "failed moveByXandY"
        moveByXandAngle 3. 4., "failed moveByXandAngle"
        moveByLengthAndAngle 4. 5., "failed second moveByLengthAndAngle"
    ]

即。 actions的类型为((float * float -> float * float) * string) list

现在,使用FSharpx我们将操作解除为选择并折叠/绑定(不确定如何调用它,这类似于foldM in Haskell)对操作:< / p>

let folder position (f,message) =
    Choice.bind (Choice.protect f >> Choice.mapSecond (konst message)) position

let finalPosition = List.fold folder (Choice1Of2 startingPosition) actions

finalPosition属于Choice<float * float, string>类型,即它是所有这些函数的最终结果,或者是错误(如上表中所定义)。

最后一个片段的说明:

  • Choice.protect类似于Tomas的protect,除了当它找到异常时,它返回包含在Choice2Of2中的异常。当没有异常时,它返回包含在Choice1Of2中的结果。
  • Choice.mapSecond使用操作表中定义的错误消息更改Choice2Of2中的此潜在异常。而不是(konst消息),这也可以是使用异常构建错误消息的函数。
  • Choice.bind针对当前位置运行此“受保护”操作。如果当前位置出错(即Choice2Of2),它将不会执行实际操作。
  • 最后,折叠应用所有动作沿着/累积结果Choice(当前位置或错误)。

所以现在我们只需要模式匹配来处理每个案例(正确的结果或错误):

match finalPosition with
| Choice1Of2 (x,y) -> 
    printfn "final position: %f,%f" x y
| Choice2Of2 error -> 
    printfn "error: %s" error

如果您取消注释上面的failwith "oops",则finalPosition将为Choice2Of2 "failed moveByXandY"

答案 3 :(得分:2)

我不清楚为什么

  

现在新要求是引入异常处理。一个大的   尝试/围绕整个运营链是不够的,因为我   想知道哪一行引起了异常。我需要知道哪个   参数无效,因此用户知道必须是什么参数   改变。

调试器不足以满足这一要求。这听起来像是用户代码中的设计时错误;这些方法中的每一个都可能抛出ArgumentException而没有任何东西可以处理它(它会使应用程序崩溃),程序员会调试并看到抛出异常的方法/堆栈,异常文本会有参数名称。

(或许这通常是FSI /脚本?)

答案 4 :(得分:0)

为什么不将异常处理放在函数调用中并抛出它们。这不会破坏代码。然后在调用它的函数中,捕获错误并显示给用户。