Pipeline sequence exception handling

I am working on a basic 2D CAD engine, and the pipeline operator has greatly improved my code. Basically, several functions start at the point (x, y) in space and calculate the final position after several movement operations:

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

It is incredibly easy to read, and I would like to keep it that way. Different x1, a1, etc. Obviously have meaning in the real code.

Now the new requirement is to introduce exception handling. A big attempt / around the whole chain of operations is not enough, because I would like to know which line caused the exception. I need to know which argument is invalid so that the user knows which parameter needs to be changed.

For example, if the first line (moveByLengthAndAngle x1 a1) throws an exception, I would like to say something like "Hey, -90 - invalid value for a1! A1 must be between 45 and 90!". Given that many operations of the same type can be used in a sequence, it is not enough to determine a different type of exception for each operation (in this example, I could not determine if the error was the first or last attempt).

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

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

(note: currently every moveBy function throws an exception in case of errors, but I can change, for example, to return an option, a larger tuple, or everything else if necessary).

+6
source share
5 answers

How about bending over a selection? Let's say that instead of pipelining to perform actions, you represent them as follows:

 let startingPosition = 0. ,0. let moveByLengthAndAngle la (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 pick up actions and add / bind ( not sure what to call it like foldM in Haskell ) over 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> , that is, it is either the final result of all these functions, or an error (as defined in the table above).

Explanation for this last snippet:

  • Choice.protect is similar to Tomas protection, except that when it finds an exception, it returns the exception enclosed in Choice2Of2. When there is no exception, it returns the result enclosed in Choice1Of2.
  • Choice.mapSecond changes this potential exception in Choice2Of2 with the error message defined in the action table. Instead of (konst message), it can also be a function that builds an error message using an exception.
  • Choice.bind launches this "protected" action against the current position. It will not take a valid action if the current position is erroneous (i.e. Choice2Of2).
  • Finally, the summary applies all the actions overlapping / accumulating the selection (either the current position or the error).

So, now we just need to match the template to handle each case (the correct result or error):

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

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

+5
source

There are many ways to approach this, the simplest would be to simply 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... 

Here is the power of expression-oriented programming :).

Unfortunately, if you pipelining through a sequence, it gets a lot more complicated than:

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

The only safe way is to make sure that the inside of each sequence operation is wrapped.

Edit: Wow, I can't believe I messed it up. This has now been fixed, but I think the other two solutions are cleaner.

+5
source

The solution described by Rick will only handle exceptions that occur when evaluating function arguments in the pipeline. However, it will not handle exceptions caused by pipelined functions (as described in response to your other question ).

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

 let times2 n = n * 2 let plus ab = 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 problem, you can write a function that wraps any other function in the exception handler. The idea that your protect function will take a function (e.g. times2 or fail ) and return a new function that takes the input signal from the pipeline (number) and passes it to the function ( times2 or fail ), but does this inside the exception handler:

 let protect msg f = fun n -> try fn 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 also handle the exceptions that occur when evaluating these functions:

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

I do not understand why

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

debugging is not enough for this. This sounds like a development-time error in user code; each of these methods can throw an ArgumentException and nothing can handle it (this will cause the application to crash), and the programmer debugs and sees the method / stack that threw the exception, and the exception text will have an argument name.

(Or maybe it's usually FSI / scripting?)

+2
source

Why not just put exception handling in function calls and throw them away. Wouldn't that break the code. Then, in your function that calls this, catch the error and show it to the user.

0
source

Source: https://habr.com/ru/post/902257/


All Articles