F# using match to validate parameters

◇◆丶佛笑我妖孽 提交于 2019-12-11 01:52:31

问题


I'm learning F#. I want to know best practices for validating input parameters. In my naivety I had thought I could do something like this:

let foo = match bar with
| <test for valid> -> bar
| _ -> "invalid"

of course that doesn't work due to mismatching types. So I'd like to see the patterns experienced F# programmers use for this sort of thing. match? If/then/else?

Something else?


回答1:


You are having problems because you are trying to bind a value to something that could be two possible types depending upon program flow - that is incompatible with static typing.

If I have some value foo, it cannot be, for example, a string OR an int depending upon program flow; it must resolve to exactly one type at compile time.

You can, however, use a discriminated union that can represent several different options within a single type.

Here is a summary of the approaches for doing just that.


Result Type / Either

F# 4.1, which is currently available via nuget, introduces the Result type. You may find this type referred to as Either in other languages.

It is defined like this:

[<Struct>] 
type Result<'T,'TError> =  
    /// Represents an OK or a Successful result. The code succeeded with a value of 'T. 
    | Ok of ResultValue:'T  
    /// Represents an Error or a Failure. The code failed with a value of 'TError representing what went wrong. 
    | Error of ErrorValue:'TError

If you are pre-F# 4.1 (which is very likely). You can define this type yourself, although you must remove the [<Struct>] attribute.

You can then make a tryParseFloat function:

let tryParseFloat str =
   match System.Double.TryParse str with
   |  true, f -> Ok f
   | _ -> Error <| sprintf "Supplied string (%s) is not a valid float" str

You can determine success or failure:

match tryParseFloat "0.0001" with
|Ok v -> // handle success
|Error err -> // handle error

In my opinion, this is the preferred option, especially in F# 4.1+ where the type is built in. This is because it allows you to include information relating to how and why some activity failed.


Option Type / Maybe

The option type contains either Some 'T or simply None. The option type is used to indicate the presence or absence of a value, None fills a role similar to null in other languages, albeit far more safely.

You may find this type referred to as Maybe in other languages.

let tryParseFloat str =
   match System.Double.TryParse str with
   |  true, f -> Some f
   | _ -> None

You can determine success or failure:

match tryParseFloat "0.0001" with
|Some value -> // handle success
|None -> // handle error

Composition

In both cases, you can readily compose options or results using the associated map and bind functions in the Option and Result modules respectively:

Map:

val map: mapping:('T -> 'U) -> option:'T option -> 'U option   
val map : mapping:('T -> 'U) -> result:Result<'T, 'TError> -> Result<'U, 'TError>

The map function lets you take an ordinary function from 'a -> 'b and makes it operate on results or options.

Use case: combine a result with a function that will always succeed and return a new result.

tryParseFloat "0.001" |> Result.map (fun x -> x + 1.0);;
val it : Result<float,string> = Ok 1.001

Bind:

val bind: binder:('T -> 'U option) -> option:'T option -> 'U option
val bind: binder:('T -> Result<'U, 'TError>) -> result:Result<'T, 'TError> -> Result<'U, 'TError>

The bind function lets you combine results or options with a function that takes an input and generates a result or option

Use case: combine a result with another function that may succeed or fail and return a new result.

Example:

let trySqrt x =
   if x < 0.0 then Error "sqrt of negative number is imaginary"
   else Ok (sqrt x)
tryParseFloat "0.001" |> Result.bind (fun x -> trySqrt x);;
val it : Result<float,string> = Ok 0.0316227766

tryParseFloat "-10.0" |> Result.bind (fun x -> trySqrt x);;
val it : Result<float,string> = Error "sqrt of negative number is imaginary"

tryParseFloat "Picard's Flute" |> Result.bind (fun x -> trySqrt x);;
val it : Result<float,string> =
  Error "Supplied string (Picard's Flute) is not a valid float"

Notice that in both cases, we return a single result or option despite chaining multiple actions - that means that by following these patterns you need only check the result once, after all of your validation is complete.

This avoids a potential readability nightmare of nested if statements or match statements.

A good place to read more about this is the Railway Oriented Programming article that was mentioned to you previously.

Exceptions

Finally, you have the option of throwing exceptions as a way of preventing some value from validating. This is definitely not preferred if you expect it to occur but if the event is truly exceptional, this could be the best alternative.




回答2:


The basic way of representing invalid states in F# is to use the option type, which has two possible values. None represents invalid state and Some(<v>) represents a valid value <v>.

So in your case, you could write something like:

let foo = 
  match bar with
  | <test for valid> -> Some(bar)
  | _ -> None

The match construct works well if <test for valid> is actual pattern (e.g. empty list or a specific invalid number or a null value), but if it is just a boolean expression, then it is probably better to write the condition using if:

let foo = 
  if <test for valid> bar then Some(bar)
  else None



回答3:


You could do something along this lines

type Bar =
    | Bar of string
    | Foo of int

let (|IsValidStr|_|) x = if x = Bar "bar" then Some x else None
let (|IsValidInt|_|) x = if x = Foo 0 then Some x else None

let foo (bar:Bar) = 
    match bar with
    | IsValidStr x -> Some x
    | IsValidInt x -> Some x
    | _ -> None

That is you could use active patterns to check for the actual business rules and return an Option instance




回答4:


Based on what the OP wrote in the comments:

You would define a type as in the post that Fyodor linked, that captures your two possible outcomes:

type Result<'TSuccess,'TFailure> = 
    | Success of 'TSuccess
    | Failure of 'TFailure

Your validation code becomes:

let checkBool str =
   match bool.TryParse str with
   |  true, b -> Success b
   | _ -> Failure ("I can't parse this: " + str)

When using it, again use match:

let myInput = "NotABool"
match checkBool myInput with
| Success b -> printfn "I'm happy: %O" b
| Failure f -> printfn "Did not like because: %s" f

If you only would like to continue with valid bools, your code can only fail on invalid arguments, so you would do:

let myValidBool = 
    match checkBool myInput with
    | Success b -> b
    | Failure f -> failwithf "I did not like the args because: %s" f


来源:https://stackoverflow.com/questions/42557725/f-using-match-to-validate-parameters

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!