Introduction to Functional Programming in F# – Part 12

Introduction

In this post we are going to look at Computation Expressions including a first look at asynchronous code in F#. It's hard to explain what they are other than syntactic sugar for simplifying code around effects like Option, Result and Async. We have actually already met one in a previous post: seq {}.

In this post we will start to learn how to use Computation Expressions, create our own simple one and look at a more complex example where we combine two effects together - Async and Result.

Setting Up

You can use any IDE. I will be using VSCode plus the ionide plugin.

Open VSCode in a new folder called ComputationExpressionDemo.

Open a new Terminal and create a new console app using:

dotnet new console -lang F#

You may have to click on the Program.fs file to get ionide started.

Introducing the Problem

Add a new .fs file above Program.fs called OptionDemo.fs. In VSCode, right-click on Program.fs and select Add File Above.

In the new file, copy the following code:

namespace ComputationExpression

module OptionDemo =

    let multiply x y = // int -> int -> int
        x * y

    let divide x y = // int -> int -> int option
        if y = 0 then None
        else Some (x / y)

    let calculate x y =
        divide x y
        |> function 
            | Some v -> multiply v x |> Some
            | None -> None
        |> function
            | Some t -> divide t y
            | None -> None

We have two simple function - multiply and divide - and we use the together to compose a bigger function. This function looks ugly with all of the Option matching. Thankfully, we know that we can use the Option module to simplify this:

let calculate x y =
        divide x y
        |> Option.map (fun s -> multiply s x)
        |> Option.bind (fun x -> divide x y)

Much nicer but what does it look like using a Computation Expression? First we will create one! Copy the following code between the namespace and the module declaration in ResultDemo.fs:

[<AutoOpen>]
module Option =

    type OptionBuilder() =
        // Supports let!
        member __.Bind(x, f) = Option.bind f x
        // Supports return
        member __.Return(x) = Some x
        // Supports return!
        member __.ReturnFrom(x) = x

    // Computation Expression for Option
    let option = OptionBuilder()

The AutoOpen attribute means that when the namespace is used, we automatically get access to the types and functions in the module.

This is a very simple Computation Expression but is enough for our needs. It's not important to know how this works but you can see that we define a type and then an instance of that type.

Replace the calculate function with the following:

let calculate x y =
        option {
            let! first = divide x y
            let second = multiply first x
            let! third = divide second y
            return third
        }

The nice thing about this function is that we don't need to know about bind or map - It's all hidden away from us, so we can concentrate on the functions.

The ComputationExpression is the option {}. Notice the bang '!' on the end of the first let. This gets handled by the Bind function in the Computation Expression. The return matches the Return in the Computation Expression. If we want to use the ReturnFrom, we do the following:

let calculate x y =
        option {
            let! first = divide x y
            let second = multiply first x
            return! divide second y
        }

Notice the bang '!' on the return and that the Computation Expression automatically handles the Option.map for use.

To test the code, replace the code in Program.fs with the following:

open ComputationExpression.OptionDemo

[<EntryPoint>]
let main argv =
    let result = calculate 8 0
    printfn "calculate 8 0 = %A" result
    printfn "result is None = %b" result.IsNone
    let result = calculate 8 2
    printfn "calculate 8 2 = %A" result
    0

Leave the 0 at the end to signify the app has completed successfully.

Now type dotnet run in the Terminal.

The Result Computation Expression

Create a new file ResultDemo.fs above Program.fs.

Rather than create our own CE for Result, we will use an existing one from the FsToolkit.ErrorHandling NuGet package.

Use the terminal to add a NuGet package that contains the Result Computation Expression that we want to use.

dotnet add package FsToolkit.ErrorHandling

Copy the following code into ResultDemo.fs:

namespace ComputationExpression

module ResultDemo =

    open FsToolkit.ErrorHandling

    type Customer = {
        Id : int
        IsVip : bool
        Credit : decimal
    }

    let getPurchases customer = // Customer -> Result<(Customer * decimal),exn>
        try
            // Imagine this function is fetching data from a Database
            let purchases = if customer.Id % 2 = 0 then (customer, 120M) else (customer, 80M)
            Ok purchases
        with
        | ex -> Error ex

    let tryPromoteToVip purchases = // Customer * decimal -> Customer
        let customer, amount = purchases
        if amount > 100M then { customer with IsVip = true }
        else customer

    let increaseCreditIfVip customer = // Customer -> Result<Customer,exn>
        try
            // Imagine this function could cause an exception
            let result = 
                if customer.IsVip then { customer with Credit = customer.Credit + 100M }
                else { customer with Credit = customer.Credit + 50M }
            Ok result
        with
        | ex -> Error ex

    let upgradeCustomer customer =
        customer 
        |> getPurchases 
        |> Result.map tryPromoteToVip
        |> Result.bind increaseCreditIfVip

Now let's see what turning the upgradeCustomer into a Computation Expression loks like by replacing it with the following:

let upgradeCustomer customer =
    result {
        let! purchases = getPurchases customer
        let promoted = tryPromoteToVip purchases
        return! increaseCreditIfVip promoted
    }

Notice that the bang '!' is only applied to those functions that apply the effect, in this case Result.

I find this style easier to read but some people do prefer the previous version.

Introduction to Async

Create a new file AsyncDemo.fs above Program.fs and copy the following code into it:

namespace ComputationExpression

module AsyncDemo =

    open System.IO

    type FileResult = {
        Name: string
        Length: int
    }

    let getFileInformation path =
        async {
            let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
            let fileName = Path.GetFileName(path)
            return { Name = fileName; Length = bytes.Length }
        }

The Async.AwaitTask is required bacause File.ReadAllBytesAsync returns a Task as it is a .Net Core function rather than F#. It is very similar in style to Async Await in C# but async in F# is lazily evaluated and Task is not.

Let's test our new code. Add the following declaration in Program.fs:

open ComputationExpression.AsyncDemo

In the main function, add the following code:

"""D:\temp\export.csv""" // Replace with a file on your system
    |> getFileInformation
    |> Async.RunSynchronously
    |> printfn "%A"

Replace my file path with one that you know exists on your system. The three quotes """ override escape sequences in a string. We have to use Async.RunSynchronously to force the async code to run.

Computation Expressions in Action

So far we have seen how to use a single effect but what happens if we wnat to use two (or more) like Async and Result? Thankfully, such a thing is possible and used regularly. We are going to use asyncResult from the FsToolkit.ErrorHandling NuGet package we installed earlier.

The idea for this example comes from the FsToolkit.ErrorHandling website:

https://demystifyfp.gitbook.io/fstoolkit-errorhandling/asyncresult/ce

Create a new file called AsyncResultDemo.fs above Program.fs and add the following code:

namespace ComputationExpression

module AsyncResultDemo =

    open System
    open FsToolkit.ErrorHandling

    type AuthError =
        | UserBannedOrSuspended

    type TokenError =
        | BadThingHappened of string

    type LoginError = 
        | InvalidUser
        | InvalidPwd
        | Unauthorized of AuthError
        | TokenErr of TokenError

    type AuthToken = AuthToken of Guid

    type UserStatus =
        | Active
        | Suspended
        | Banned

    type User = {
        Name : string
        Password : string
        Status : UserStatus
    }

AuthToken is a single case discriminated union. It could have been written like TokenError but is generally written as seen. The first AuthToken is the name of the type, the second is the constructor.

Now we will add some constants, marked with the Literal attribute:

[<Literal>]
    let ValidPassword = "password"
    [<Literal>]
    let ValidUser = "isvalid"
    [<Literal>]
    let SuspendedUser = "issuspended"
    [<Literal>]
    let BannedUser = "isbanned"
    [<Literal>]
    let BadLuckUser = "hasbadluck"
    [<Literal>]
    let AuthErrorMessage = "Earth's core stopped spinning"

And now we add the core functions, some of which are async and some return Results. Don't worry about the implementations, concentrate of the return types:

let tryGetUser (username:string) : Async<User option> =
        async {
            let user = { Name = username; Password = ValidPassword; Status = Active }
            return
                match username with
                | ValidUser -> Some user
                | SuspendedUser -> Some { user with Status = Suspended }
                | BannedUser -> Some { user with Status = Banned }
                | BadLuckUser -> Some user
                | _ -> None
        }

    let isPwdValid (password:string) (user:User) : bool =
        password = user.Password

    let authorize (user:User) : Async<Result<unit, AuthError>> =
        async {
            return 
                match user.Status with
                | Active -> Ok ()
                | _ -> UserBannedOrSuspended |> Error 
        }

    let createAuthToken (user:User) : Result<AuthToken, TokenError> =
        try
            if user.Name = BadLuckUser then failwith AuthErrorMessage
            else Guid.NewGuid() |> AuthToken |> Ok
        with
        | ex -> ex.Message |> BadThingHappened |> Error

The final part is to add the main login function that uses the previous functions and the asyncResult CE. The function does four things - tries to get the user, checks the password is valid, checks the authorization and creates a token to return:

let login (username: string) (password: string) : Async<Result<AuthToken, LoginError>> =
        asyncResult {
            let! user = username |> tryGetUser |> AsyncResult.requireSome InvalidUser
            do! user |> isPwdValid password |> Result.requireTrue InvalidPwd
            do! user |> authorize |> AsyncResult.mapError Unauthorized
            return! user |> createAuthToken |> Result.mapError TokenErr
        }

The return type from the function is Async<Result<AuthToken, LoginError>>, so asyncResult really is Async wrapping Result. This is very common in LOB app writing in F#.

Notice the use of functions from the referenced library which make the code nice and succinct. The do! = supports functions that return unit and is the same as writing let! _ =. The mappError functions are used to convert the errors from the authorize and createAuthToken functions to the LoginError type used by the login function.

To test the code, copy the following under the existing code in AsyncResultDemo.fs or create a new file called AsyncResultDemoTests.fs below AsyncResultDemo.fs and above Program.fs.

module AsyncResultDemoTests =

    open AsyncResultDemo

    [<Literal>]
    let BadPassword = "notpassword"
    [<Literal>]
    let NotValidUser = "notvalid"

    let isOk (input:Result<_,_>) : bool =
        match input with
        | Ok _ -> true
        | _ -> false

    let matchError (error:LoginError) (input:Result<_,LoginError>) =
        match input with
        | Error ex -> ex = error
        | _ -> false  

    let runWithValidPassword (username:string) = 
        login username ValidPassword |> Async.RunSynchronously

    let success =
        let result = runWithValidPassword ValidUser
        result |> isOk 

    let badPassword =
        let result = login ValidUser BadPassword |> Async.RunSynchronously
        result |> matchError InvalidPwd

    let invalidUser =
        let result = runWithValidPassword NotValidUser
        result |> matchError InvalidUser

    let isSuspended =
        let result = runWithValidPassword SuspendedUser
        result |> matchError (UserBannedOrSuspended |> Unauthorized)

    let isBanned =
        let result = runWithValidPassword BannedUser
        result |> matchError (UserBannedOrSuspended |> Unauthorized)

    let hasBadLuck =
        let result = runWithValidPassword BadLuckUser
        result |> matchError (AuthErrorMessage |> BadThingHappened |> TokenErr)

In Program.fs main function copy the following:

printfn "Success: %b" success
printfn "BadPassword: %b" badPassword
printfn "InvalidUser: %b" invalidUser
printfn "IsSuspended: %b" isSuspended
printfn "IsBanned: %b" isBanned
printfn "HasBadLuck: %b" hasBadLuck

In the terminal, type dotnet run and press enter. You should see successfull tests.

If you put a breakpoint in the createAuthToken function on the if statement and debug the code, you will see that you only hit the breakpoint twice, on success and hasbadluck cases.

The code for the last section can be found at:

https://gist.github.com/ianrussellsoftwarepark/2d11367c69d5f14231d439580034742d

Further Reading

Computation Expressions are very useful tools to have at your disposal and are well worth investigating further.

It is vital that you learn more about how F# handles asynchronous code with async and how it interacts with the Task type from .Net Core. You can find out more at the following link:

https://docs.microsoft.com/en-us/dotnet/fsharp/tutorials/asynchronous-and-concurrent-programming/async

Conclusion

In this post we have had a first look at Computation Expressions in F#. They can be a little confusing to begin with but make for extremely readable code.

If you have any comments on this series of posts or suggestions for new ones, send me a tweet (@ijrussell) and let me know.

Part 11 Table of Contents

Zurück
Zurück

Introduction to Functional Programming in F# – Table of Contents

Weiter
Weiter

Understanding F# Type Aliases