问题
I have been teaching myself F# lately, and I come from an imperative (C++/C#) background. As an exercise I have been working on functions that can do stuff with matrices, like add, multiply, get determinants, etc. Everything is going well in this regard, but I find that maybe I am not making the best decisions when it concerns handling invalid inputs, for example:
// I want to multiply two matrices
let mult m1 m2 =
let sizeOK = validateDims m1 m2
// Here is where I am running to conceptual trouble:
// In a C# world, I would throw an exception.
if !sizeOK then
raise (InvalidOperationException("bad dimensions!")
else
doWork m1 m2
So while this technically works, is this appropriate for a functional language? Is it in the spirit of functional programming? Or would it make more sense to rewrite it as:
let mult m1 m2 =
let sizeOK = validateDims m1 m2
if !sizeOK then
None
else
Some doWork m1 m2
In this case I am returning an option, which adds an extra layer around the matrix, but I could also use the results of the function, even in failure cases (None) with pattern matching, etc. at some later point in the program. So is there a best practice for these types of scenarios? What would a functional programmer do?
回答1:
I tend to avoid exceptions for the following reasons:
- .NET exceptions are slow
- Exceptions change control flows of programs in an unexpected way, which makes it much harder to reason about
- Exceptions often arise in critical situations while you can fail-safe by using options.
In your case, I will follow F# core library conventions (e.g. List.tryFind
and List.find
, etc.) and create both versions:
let tryMult m1 m2 =
let sizeOK = validateDims m1 m2
if not sizeOK then
None
else
Some <| doWork m1 m2
let mult m1 m2 =
let sizeOK = validateDims m1 m2
if not sizeOK then
raise <| InvalidOperationException("bad dimensions!")
else
doWork m1 m2
This example isn't exceptional enough to use exceptions. The mult
function is included for C# compatibility. Someone using your library in C# doesn't have pattern matching to decompose options easily.
One drawback with options is that they don't give the reason why the function didn't produce a value. It's overkill here; generally Choice (or Either monad in Haskell term) is more suitable for error handling:
let tryMult m1 m2 =
// Assume that you need to validate input
if not (validateInput m1) || not (validateInput m2) then
Choice2Of2 <| ArgumentException("bad argument!")
elif not <| validateDims m1 m2 then
Choice2Of2 <| InvalidOperationException("bad dimensions!")
else
Choice1Of2 <| doWork m1 m2
It's a pity that F# Core lacks high-order functions to manipulate Choice. You can find those functions in FSharpX or ExtCore library.
回答2:
I tend to go with the following guidelines:
Use exception in a function that is supposed always have a return values, when something goes wrong unexpectedly. This could e.g. be if the arguments does not obey the contract for the function. This has the advantage that client code gets simpler.
Use an Option when the function sometimes has a return value for valid input. This could e.g. be get on a map where a valid key might not exist. Thereby you force the user to check if the function has a return value. This might reduce bugs, but always clutters the client code.
Your case is somewhat in between. If you expect it primarily to be used in places where the dimensions are valid, I would throw an exception. If you expect client code to often call it with invalid dimension I would return an Option. I will probably go with the former, as it is cleaner (see below) but I don't know your context:
// With exception
let mult3 a b c =
mult (mult a b) c;
// With option
let mult3 a b c=
let option = mult a b
match option with
| Some(x) -> mult x b
| None -> None
Disclaimer: I have no professional experience with functional programming, but I'm a TA in F# programming on a graduate level.
回答3:
I like the above answers but I wanted to add another option. It really depends how unexpected the outcome is and whether it makes sense to proceed. If it's a rare event and the caller likely didn't plan to fail, then an exception is totally respectable. The code to catch the exception may be many levels above and the caller probably didn't plan to fail. If it's a really routine result for an operation to fail, Some/None is ok though it gives you just two options and no way of passing a result. Another option is to make a discriminated union of possibilities. This forces the caller likely to match on the different outcomes, is extensible and doesn't force you to make every result the same data type.
e.g.
type MultOutcome =
| RESULT of Matrix
| DIMERROR
| FOOERROR of string
let mult a b =
if dimensionsWrong then
DIMERROR
elif somethingElseIDoNotLike then
FOOERROR("specific message")
else
DIMRESULT(a*b)
match mult x y with
| DIMERROR -> printfn "I guess I screwed up my matricies"
| FOOERROR(s) -> printfn "Operation failed with message %s" s
| DIMRESULT(r) ->
// Proceed with result r
来源:https://stackoverflow.com/questions/18469815/f-some-none-or-exception