管道序列中的异常处理

Fra*_*ori 6 f#

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

let finalPosition =
    startingPosition
    |> moveByLengthAndAngle x1 a1 
    |> moveByXandY x2 y2
    |> moveByXandAngle x3 a3
    |> moveByLengthAndAngle x4 a4
    // etc...
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

有没有办法满足这个要求,而不会牺牲当前代码的可读性和优雅性?

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

Ric*_*ich 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...
Run Code Online (Sandbox Code Playgroud)

看到表达式编程的力量:).

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

  1. 管道中发生的事情(对于Seqs)是组合,而不是执行.
  2. IEnumerable中的异常处理是未定义的,因此取决于Enumerator的实现.

唯一安全的方法是确保包装每个序列操作的内部.

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


Tom*_*cek 5

Rick描述的解决方案只是处理在评估管道中函数的参数时引发的异常.但是,它不会处理由流水线函数引发的异常(如回答您的其他问题所述).

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

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'!
Run Code Online (Sandbox Code Playgroud)

要解决此问题,您可以编写一个函数来包装异常处理程序中的任何其他函数.你的protect函数将获取一个函数(例如times2fail)的想法,并将返回一个新函数,该函数从管道(数字)获取输入并将其传递给函数(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
Run Code Online (Sandbox Code Playgroud)

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

let n =
  10 |> protect "Times" times2
     |> protect "Fail" fail
     |> protect "Plus" (plus 5)
Run Code Online (Sandbox Code Playgroud)


Mau*_*fer 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"
    ]
Run Code Online (Sandbox Code Playgroud)

actions属于类型((float * float -> float * float) * string) list.

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

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

let finalPosition = List.fold folder (Choice1Of2 startingPosition) actions
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

如果你取消failwith "oops"上面的注释,则finalPosition将是一个Choice2Of2 "failed moveByXandY"