Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Exception handling in pipeline sequence

Tags:

f#

I' working on a basic 2D CAD engine and the pipeline operator significantly improved my code. Basically several functions start with a point (x,y) in space and compute a final position after a number of move operations:

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

This is incredibly easy to read and I'd like to keep it that way. The various x1, a1, etc. obviously have a meaning name in the real code.

Now the new requirement is to introduce exception handling. A big try/with around the whole operation chain is not enough because I'd like to know which line caused the exception. I need to know which argument is invalid, so that the user knows what parameter must be changed.

For example if the first line (moveByLengthAndAngle x1 a1) raises an exception, I'd like to tell something like "Hey, -90 is an invalid value for a1! a1 must be between 45 and 90!". Given that many operations of the same type can be used in the sequence it's not enough to define a different exception type for each operation (in this example I wouldn't be able to tell if the error was the first or the last move).

The obvious solution would be to split the chain in single let statements, each within its respective try/with. This however would make my beautiful and readable code a bit messy, not so readable anymore.

Is there a way to satisfy this requirement without sacrificing the readability and elegance of the current code?

(note. right now every moveBy function raises an exception in case of errors, but I'm free to change for ex. to return an option, a bigger tuple, or just anything else if needed).

like image 958
Francesco De Vittori Avatar asked Nov 23 '11 22:11

Francesco De Vittori


3 Answers

The solution that Rick described is only going to handle exceptions that are raised when evaluating the arguments of the functions in the pipeline. However, it will not handle the exceptions that are raised by the pipelined functions (as described in answer to your other question).

For example, let's say you have these simple functions:

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'!

To solve this, you can write a function that wraps any other function in an exception handler. The idea that your protect function will take a function (such as times2 or fail) and will return a new function that takes the input from the pipeline (number) and passes it to the function (times2 or fail), but will do this inside exception handler:

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

Now you can protect each function in the pipeline and it will also handle exceptions that happen when evaluating these functions:

let n =
  10 |> protect "Times" times2
     |> protect "Fail" fail
     |> protect "Plus" (plus 5)
like image 157
Tomas Petricek Avatar answered Nov 08 '22 07:11

Tomas Petricek


How about folding over Choices? Let's say that instead of pipelining the actions, you represent them like this:

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"
    ]

i.e. actions is of type ((float * float -> float * float) * string) list.

Now, using FSharpx we lift the actions to Choice and fold/bind (not sure how to call it this is similar to foldM in Haskell) over the actions:

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

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

finalPosition is of type Choice<float * float, string> , i.e. it's either the final result of all those functions, or an error (as defined in the table above).

Explanation for this last snippet:

  • Choice.protect is similar to Tomas' protect, except that when it finds an exception, it returns the exception wrapped in a Choice2Of2. When there's no exception, it returns the result wrapped in a Choice1Of2.
  • Choice.mapSecond changes this potential exception in Choice2Of2 with the error message defined in the table of actions. Instead of (konst message) this could also be a function that builds the error message using the exception.
  • Choice.bind runs this "protected" action against the current position. It will not run the actual action if the current position is in error (i.e. a Choice2Of2).
  • Finally, the fold applies all actions threading along / accumulating the resulting Choice (either the current position or an error).

So now we just have to pattern match to handle each case (correct result or error):

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

If you uncomment failwith "oops" above, finalPosition will be a Choice2Of2 "failed moveByXandY"

like image 33
Mauricio Scheffer Avatar answered Nov 08 '22 06:11

Mauricio Scheffer


There's a lot of ways to approach this, the easiest would be to just wrap each call in a try-with block:

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...

Behold the power of expression oriented programming :).

Unfortunately, if you're pipelining over a sequence it becomes much more difficult as:

  1. What happens in the pipeline (for Seqs) is composition, not execution.
  2. Exception handling inside an IEnumerable is undefined and so depends on the implementation of the Enumerator.

The only safe way is to make sure the internals of each sequence operation are wrapped.

Edit: Wow, I can't believe I messed that up. It's fixed now but I do think that the two other solutions are cleaner.

like image 30
Rick Minerich Avatar answered Nov 08 '22 08:11

Rick Minerich