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).
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)
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:
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"
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:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With