문제

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?

도움이 되었습니까?

해결책

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.

다른 팁

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

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.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top