Functional programming for railway

Time:2021-1-26

original texthttps://fsharpforfunandprofit.com/posts/recipe-part2/
reference resourceshttps://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/results

This is the most popular article “railway oriented programming” on one of the most popular websites of F #.

The code is not long. Let’s look at the code first. I’ll write the explanation at the back of the code.

type Request = {Name:string; Email:string}

let validateName request =
    match request with
    | {Name=name; Email=_} when name = "" ->
        Error "name must not be blank"
    | _ -> Ok request

let validateEmail =
    function
    | {Name=_; Email=email} when email = "" -> Error "email must not be blank"
    | request -> Ok request

let test1() =
    let validate =
        Result.bind validateName >> Result.bind validateEmail

    let result1 = validate (Ok {Name="abc"; Email="[email protected]"})
    printfn "%A" result1

    let result2 = validate (Ok {Name="abc"; Email=""})
    printfn "%A" result2

test1()

After installing the. Net SDK, copy the above code, paste it into a file and save it as railway.fsx To use the command in the consoledotnet fsi railway.fsxIt is ready to run.

The purpose of this code is to validate the request and handle the error gracefully.

In order to keep it simple, we only do two simple verifications, but in reality, we may need to do many verifications for the same request, and every step may produce errors, so we must find a way to handle the errors gracefully.

In functional programming, if the output of function F1 can be used as the input parameter of function F2, then F1 and F2 can be directly spliced into F3

Therefore, as long as we try to make the input and output of each verification function the same, we can easily put them together.

A feasible way is to use the standard library Result.bind Function(https://github.com/dotnet/fsharp/blob/main/src/fsharp/FSharp.Core/result.fs)

The bind function is the key to the code at the beginning of this article, and also the key to railway oriented programming!

About Result.bind function

This function takes two arguments: FN and result.

The type of result is result, which has two possibilities: OK or error.

When the result is OK, the function fn is used to process it; when the result is error, FN is not executed.

Finally, the bind function also returns a result.

In short, the function of bind is to ensure that we can always input a result, and after FN processing, we can always output a result.

About validatename and validateemail

After understanding the role of the bind function, the next thing is very easy to understand.

Look at validatename and validateemail, where validateemail uses a syntaxfunctionIn fact, it is the same as in validatenamematch...withThe function of the grammar is exactly the same. I just want to introduce the grammar sugar here by the way.

Although both functions output a result, their input parameters are request instead of result, so they can’t be spliced directly.

At this point, we use bind to see what we get(Watch out. Something magical is about to happen

let validate1 = Result.bind validateName
let validate2 = Result.bind validateEmail

Because the type of bind isfn -> Result -> Result(where FN is a function whose return value is also a result)

So, when we feed it a FN function, it becomesResult -> Result

In other words, validate1 isResult -> ResultAnd so is validate2Result -> Result

In other words, once they are binded, they magically unify the input and output. Now they can be directly spliced

let validate = Result.bind validateName >> Result.bind validateEmail

Why is it called “railway oriented programming”?

There is a diagram in the original text, which shows the similarities between this programming mode and railway. If you are interested, please see the original text.

Here I just want to make the point: this kind of railway has two tracks, one is OK, the other is error.

Data is like a passenger and function is like a station. The data is on the OK track at the starting point, and some processing is carried out at each station (the data is processed by the function and then transferred to the next function). If there is an error, switch to the error track.

An important feature is that the error track is like a fast lane. Once you switch to the error track, you will never stop until you reach the destination.

epilogue

At this point, all the fog has been solved, please look back at the code at the beginning of this article, I believe you can easily understand it now.

Update and supplement: another railway splicing method

We can also customize an operator>=>To do the stitching.

let (>=>) f1 f2 x =
    match f1 x with
    | Ok y -> f2 y
    | Error z -> Error z

let test2() =
    let validate =
        validateName >=> validateEmail

    let result1 = validate {Name="abc"; Email="[email protected]"}
    printfn "%A" result1

    {Name="abc"; Email=""}
    |> validate
    |> printfn "%A"

test2()

Here, the focus is on the validate function, which is composed of two functions with the same signature, and the final validate signature is the same as each of them.

For example, in this example, the signatures of validatename and validateemail are request > result, so the spliced validate is also request > result

It’s easy to use Result.map function

We’ve talked about it before Result.bind Function, and Result.map Similar to it.

The function of bind is to transform a function ‘a – > result’ into result – > result, while the function of map is to transform an ordinary function ‘a – >’b (which is completely different from result) into result – > result. For example:

// Request -> Request
let canonicalizeEmail input =
   { input with Email = input.Email.Trim().ToLower() }

// Result -> Result
let validateAndProcess =
    Result.bind validateName
    >> Result.bind validateEmail
    >> Result.map canonicalizeEmail

(Note: for the sake of simplicity in expression and convenience in understanding, there are many places that are not rigorous enough in this article. Please refer to the original for more detailed and rigorous contents.)