4

Validation with F# 5 and FSToolkit

 3 years ago
source link: https://www.compositional-it.com/news-blog/validation-with-f-5-and-fstoolkit/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Things don't always work out the way we planned

This is true in most walks of life, and it is also true in our code. Whenever we deal with an indeterminate state, such as processing user input or calling an external API, we have to consider the possible different outcomes.

Getting Results

In F#, if a function may possibly succeed or fail, we usually model this using the included Result<'a,'b> type, where 'a is the type we expect for the Ok case, and 'b for the Error case.

We often find ourselves in a situation where our workflow consists of a chain of functions, each of which requires the output of the previous, and each may fail (and hence returns a Result).

This is where we would often reach for the Result.bind function. This executes a given function, and if it returns Ok, passes its output to the input of the next Result-returning function in the chain.

However, in the case that any of the operations return an Error, the remaining functions are not executed. The Error is returned immediately.

You may have heard of this as Railway Oriented Programming, and it is a monadic way of dealing with Results.

open System

let random = Random()

let callIntegerApi () = 
    if random.Next() % 7 = 0 then
        Error "Something went wrong"
    else
        Ok (random.Next(), random.Next())

let tryDivideInteger (x, y) =
    if x = 0 || y = 0 then
        Error "Can't divide by Zero"
    else
        Ok (x / y)

let stringEvenNumbers x = 
    if x % 2 = 0 then
        Ok $"{x}" // F# 5 string interpolation ๐Ÿ˜Š
    else 
        Error "Can't parse odd numbers"
callIntegerApi ()
|> Result.bind tryDivideIntegers
|> Result.bind stringEvenNumbers

Running this code a few times in F# Interactive shows different results:

val it : Result<string,string> = Ok "4"
...
val it : Result<string,string> = Error "Can't parse odd numbers"

Sharpening our Tools

An alternative way of using Result.bind is as a computation expression.

result {
    let! randomIntPair = callIntegerApi ()
    let! divideResult = tryDivideIntegers randomIntPair
    return! stringEvenNumbers divideResult
}

This Result expression is not available out-of-the-box with FSharp, but it is included, along with many other essential tools, in a fantastic library called FSToolkit.

Needing Validation

In the previous example, aborting the workflow on the first error made sense.

Each function depended on the result of the previous, so if any failed then there was no other option.

This isn't always the case though. Sometimes we have a bunch of functions we want to execute independently, perhaps in parallel, and then we want to use all of the results for a single operation at the end.

We either want to get the desired outcome, or a list of all the errors that occured, not just the first.

This is an applicative way of dealing with Results, as opposed to the monadic approach we saw earlier.

A common example of this kind of workflow is form validation.

It would be very frustrating when filling in a form if you made multiple errors but were only told of the first one each time you pressed submit. You want to submit the form once and find out all of the things which need fixing.

Well, FSToolkit comes to the rescue again. It now provides a validation computation expression which leverages the new and! operator added to F#5. This allows it to act applicatively, rather than monadically.

#r "nuget: FSToolkit.ErrorHandling" // F# 5 direct nuget references in scripts ๐Ÿ˜Š
open FsToolkit.ErrorHandling
open System

type Customer =
    { Name : string
      Height : int
      DateOfBirth : DateTime }

let validateName name = 
    if String.IsNullOrWhiteSpace name then
        Error "Name can't be empty"
    else
        Ok name

let validateHeight height = 
    if height > 0 then
        Ok height
    else
        Error "Everything has a height"

let validateDateOfBirth dob =
    if dob < DateTime.UtcNow then
        Ok dob
    else
        Error "You can't be born in the future"

let validateCustomerForm name height dob =
    validation {
        let! validName = validateName name
        and! validHeight = validateHeight height
        and! validDob = validateDateOfBirth dob
        return { Name = validName; Height = validHeight; DateOfBirth = validDob }
    }
validateCustomerForm "Joe Bloggs" 180 (DateTime(1980, 4, 1))
val it : Validation<Customer,string> =
  Ok { Name = "Joe Bloggs"
       Height = 180
       DateOfBirth = 01/04/1980 00:00:00 { Date = 01/04/1980 00:00:00;
                                           Day = 1;
                                           DayOfWeek = Tuesday;
                                           DayOfYear = 92;
                                           Hour = 0;
                                           Kind = Unspecified;
                                           Millisecond = 0;
                                           Minute = 0;
                                           Month = 4;
                                           Second = 0;
                                           Ticks = 624589920000000000L;
                                           TimeOfDay = 00:00:00;
                                           Year = 1980; } }
validateCustomerForm "" 0 (DateTime(1980, 4, 1))
val it : Validation<Customer,string> =
  Error ["Name can't be empty"; "Everything has a height"]

As you can see, this is a really concise and clear way of modelling validation. It is also a great example of the power of applicatives, which are now even easier to use with the arrival of F# 5.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK