Trustbit

View Original

Introduction to Functional Programming in F# – Part 4

Introduction

Welcome to the fourth post in this introductory series on functional programming in F#. In this post we will be building on the function composition concepts we worked through in the previous post and we'll be writing our first unit tests in F#. As an added bonus we will also start looking at how we can separate our code into discrete modules.

Getting Started

We're going to use Visual Studio 2019 Community Edition for this post but everything will also work in VSCode + ionide and Jetbrains Rider.

  1. Create a new folder to store your code.

  2. From the Start screen, select F# from the languages dropdown and Library from the Project Type dropdown. Select the Class Library (.NET Core) option and click Next. Name the project DemoCode and the solution MyDemoCode and select your new folder from the selector. Click on Create.

  3. Once the project has been created, right click on the solution and select Add -> New Project. Select F# from the language dropdown and Test from the Project Type dropdown. Select the xUnit Test Project (.NET Core) and click next. Call the project DemoCodeTests and make sure the folder is the same as for the other project. Click on Create.

  4. We need to add/update some NuGet packages. Right click on the solution and choose Manage Nuget Packages for Solution. Update any packages NuGet informs you of and install the FsUnit and FsUnit.Xunit packages to the test project.

  5. Rename Library.fs in the DemoCode project to Customer.fs and rename Tests.fs in the test project to CustomerTests.fs.

  6. Right click on Dependencies in the test project and add a Project Reference to the DemoCode project.

  7. Build the solution.

Now that we have everything set up, let's start with the code.

Namespaces and Modules

Open Customer.fs in the DemoCode project, clear any existing code and type in the following at the top:

See this content in the original post

Each F# file (.fs) will have a namespace at the top. It serves the same purpose as it does in the rest of the .Net ecosystem.

Next we add the type definitions:

See this content in the original post

A module is a container that we can group related things into, in this case the customer type definitions. The AutoOpen attribute means that the module is loaded and available to all the following code. It's a useful option for common types or functions. Exn is the built-in F# exception type.

Next we add another module below the CustomerTypes module that contains a dummy datastore. We will be looking at using a real database later in another post.

See this content in the original post

The square brackets signify an F# list - in this case a list of customers. We will look at collections in more detail in the next post in this series.

Let's add a function to upgrade a customer to Eligible in a module below the Db module:

See this content in the original post

Note that we are returning the original customer if they are already eligible for the purposes of this example code but we would probably handle that use case differently in a real program.

The next stage is to write an upgrade customer function that used the Db module:

See this content in the original post

At the moment we have a problem because the output of Db.tryGetCustomer (Result) is different that the input of convertToEligible (Customer). Let's create a function that would allow us to compose these two functions together:

See this content in the original post

This looks very similar to the map function we built in the last post about Result:

See this content in the original post

Let's modify our function so that it matches the style of the map function:

See this content in the original post

Now let's do some renaming:

See this content in the original post

As with the Result.map, there is also an Option.map as it is such a common thing. We still need to fix the Result issue which we do by using Result.map as convertToEligible doesn't output a Result:

See this content in the original post

As before, fixing one problem has led to the next one! Again, let's write a function to deal with the option issue first.

See this content in the original post

This works but isn't testable without the Db. Let's pass (inject) the service as a function into our new function:

See this content in the original post

Then we need to plug the new function into the pipeline with a Result.bind:

See this content in the original post

We have now fixed the errors and should have a Customer module that looks like this:

See this content in the original post

Now we can add a simple assert to check that it works. Rather than add the assert into the Customer.fs file, we will create a new script file called Asserts.fsx in the DemoCode project by right-clicking on the DemoCode project an selecting Add -> New item and selecting a Script File (.fsx) and call it CustomerAsserts.fsx. Once this has been done we add the following code to the top of the file:

See this content in the original post

This loads the code from Customer.fs. Highlight the line and press ALT + ENTER to load the code into FSI. It may take a few seconds. Now add the following code below the #load line and run in FSI:

See this content in the original post

Note that the open statements use .

The code in Tests.fsx will not get compiled into the output dll but it allows us to separate simple test/assert code from our production code. Run the assert in FSI.

Creating Unit Tests

Copy the following code into CustomerTests.fs in the DemoCodeTests project.

See this content in the original post

Rather than re-invent the wheel, in this case we are using Xunit as our test framework but we are using FsUnit for assertions as they are easier to read.

The nice thing about F# for naming things, particularly tests, is that we can use the double backtick to allow us to use readable sentences rather than cased code.

If you build the solution, you will be able to see the tests in VS 2019 Test Explorer. Run the tests and watch them go green. Change the IsEligible value to false on the sourceCustomer and run the tests again. Have a look at the way that the errors are reported in Test Explorer. Revert the IsEligible value back to true.

Extending the Functionality

Let's add a couple of functions to register a customer:

See this content in the original post

Again we have a small problem as the output of Db.tryGetCustomer (Result) doesn't match the input of createCustomer (string), so we need to create a new function to compose them together that returns an exception if a customer with that Id already exists:

See this content in the original post

There isn't anything built into F# to help with this but it is a common pattern in this style of programming.

Our new adaptor function (tryCreateCustomer) returns a Result, so we need to use Result.bind rather than Result.map. This fixes the initial problem but shows the next one on the save to the database:

See this content in the original post

Fortunately, this is a really simple fix - just add a Result.bind - as tryCreateCustomer returns a Customer on the Ok track and saveCustomer takes a Customer:

See this content in the original post

We can write a simple assert function in the CustomerAsserts.fsx file:

See this content in the original post

Highlight the #load line and run in FSI and then highlight the rest of the code and run that in FSI. You should get two true statements.

Adding Unit Tests for Register Customer

Add the following code between the open statements and the module in the CustomerTests.fs file. These are simple helper functions to make the next tests more readable:

See this content in the original post

Now we can add the unit tests for registration below the existing module:

See this content in the original post

The only new feature in these tests is the ||> operator which passes a tuple of two items as two individual arguments to the function on the right.

Now that we have tests, we can delete the asserts in CustomerAsserts.fsx as they are no longer needed.

The Completed Codebase

The following is what you should now have in Customer.fs:

See this content in the original post

The following is what you should now have in CustomerTests.fs:

See this content in the original post

Summary

I hope you enjoyed this post. The features we used are important if we want to write bigger codebases in F#.

  • Namespaces and modules

  • Unit testing with Xunit

  • Assertions with FsUnit

In the next post in this series, we will have a look at collections.

Part 3 Table of Contents Part 5