Trustbit

View Original

Introduction to Functional Programming in F#

Introduction

This series of posts will introduce you to the world of functional programming (FP) in F#. Rather than start with theory or a formal definition, I thought that I'd start with a typical business problem and look at how we can use some of the functional programming features of F# to solve it.

Setting up your environment

  1. Install F# (Installing the dotnet core SDK will install F#)

  2. Install VSCode with the ionide extension (VS2019 or JetBrains Rider will work as well)

  3. Open VSCode and open a blank folder to store your code.

  4. Add a new file and name it first.fsx

  5. Type 1 = 1 into the file.

  6. Highlight the code and press ALT + ENTER

  7. You should see F# Interactive (FSI) open in your Terminal and be able to see 'val it : bool = true'

If all is OK, let's take a look at a simple business Use Case and see how we can use functional programming in F# to implement it.

Stage 1 - The Problem

This problem comes from a post by Chris Roff (https://medium.com/@bddkickstarter/functional-bdd-5014c880c935) where he looks at using F# and BDD together.

See this content in the original post
When <Customer Id> spends <Spend>
Then their order total will be <Total>
See this content in the original post

Along with some examples showing how you can verify that your code is working correctly are a number of domain-specific words and concepts. I want to show how we can represent some of these in our code. We will start of with something simple but naive and then we'll see how F# can help us make it much more domain specific and as an added benefit, less susceptible to bugs.

Stage 2 - Initial Version:

Along with simple datatypes like string, decimal and boolean, F# has a powerful Algebraic Type System (ATS). At this stage, think of these types as data structures to use in functions. The first of the types we will use is the Record Type. We can define our customer like this:

See this content in the original post

To create an instance of a customer we would write the following below the type definition:

See this content in the original post

By using the let keyword, we have bound the name 'fred' to the this instance of a Customer. It is immutable (cannot be changed).

Delete fred as we don't need him.

Below the Customer type, we need to create a function to calculate the total. The function should take a Customer and a Spend (decimal) and return the Total (decimal).

See this content in the original post

There are a few things to note about functions:

  • We have used 'let' again to define the function and inside the function to define discount and total.

  • There is no container as functions are first-class citizens.

  • The return type is to the right of the input arguments.

  • No return keyword. The last line is returned.

  • Significant whitespace (Tabs are not allowed).

  • The function signature is Customer -> decimal -> decimal. The item at the end of the signature (after the last arrow) is the return type of the function.

Function Signatures are very important; Get used to looking at them.

The F# Compiler uses a feature called Type Inference which means that most of the time it can determine types through usage without you needing to explicitly define them. As a consequence, we can re-write the function as:

See this content in the original post

I also removed the total binding as I don't think it adds anything to the readability of the function. The function signature is still Customer -> decimal -> decimal.

Highlight the code you've written so far and press ALT + ENTER. This will run this code in F# Interactive (FSI) in the Terminal window.

Now create a customer from our specification and run in FSI:

See this content in the original post

Rather than write a formal test, we can use FSI to run simple verifications for us. We will look at writing proper unit tests later in the series.

See this content in the original post

What you should see after running the test in FSI is the following:

See this content in the original post

Add in the other users and test cases from the specification.

See this content in the original post

Highlight the new code and press ALT + ENTER. You should see the following in FSI.

See this content in the original post

Your code should now look like this:

See this content in the original post

Whilst this code works, I don't like boolean properties representing domain concepts. To this end, we will make Registered/Unregistered explicit in the code.

Stage 3 - Making the Implicit Explicit (1)

Firstly, we create specific Record types for Registered and Unregistered Customers.

See this content in the original post

To represent the fact that a Customer can be either Registered or Unregistered, we will use another of the built-in types in the ATS; the Discriminated Union (DU). We define the Customer type like this:

See this content in the original post

It is very hard to describe a Discriminated Union to an OOP developer because there is nothing in OOP that is remotely close to them. This reads as "a customer is either a registered customer of type RegisteredCustomer or a guest of type UnregisteredCustomer".

The easiest way to understand a DU is to use it! We have to make changes to the users that we have defined. Firstly the UnregisteredCustomer:

See this content in the original post

Look at how the definition in the DU compares to the binding.

Now let's make the required changes to the RegisteredCustomers:

See this content in the original post

Changing the Customer type to a DU has an impact on the function. We will need to re-write the discount calculation using another F# feature - Pattern Matching:

See this content in the original post

To understand what the pattern match is doing is matching, compare the match 'RegisteredCustomer c' with how we constructed the users 'RegisteredCustomer { Id = "John"; IsEligible = true }'. In this case, 'c' is a placeholder for the customer instance. The underscore in the Guest pattern match is a wildcard and implies that we don't need access to the instance. Pattern matching against DUs is exhaustive. If you don't handle every case, you will get a warning on the customer in the match saying 'incomplete pattern match'.

We can simplify the logic with a guard clause but it does mean that we need to account for non-eligible Registered customers otherwise the match is incomplete:

See this content in the original post

We can simplify the last two matches using a wildcard like this:

See this content in the original post

The tests don't need to change.

This is much better than the naive version we had before. It is much easier to understand the logic and much more difficult to have data in an invalid state. Does it get better if we make Eligibility explicit as well? Let's see!

Stage 4 - Making the Implicit Explicit (2)

Remove the IsEligible flag from RegisteredCustomer and add EligibleRegisteredCustomer to the Customer DU.

See this content in the original post

We no longer need to test for IsEligible and we also no longer need access to the instance, so we can replace the 'c' with a underscore (wildcard).

We make some minor changes to our helpers.

See this content in the original post

I think that this is a big improvement over where we started but we can do better! We will revisit this in a later post and we will look at Unit Testing where we can make use of the helpers and assertions we've already written.

Summary

We have covered quite a lot in this post:

  • F# Interactive (FSI)

  • Algebraic Type System

    • Record Types

    • Discriminated Union

  • Pattern Matching

    • Guard Clause

  • Let bindings

  • Functions

  • Function Signatures

In the next post, we will start to look at function composition - building bigger functions out of smaller ones.

Postscript

To illustrate the portability of the functional programming concepts we have covered in this post, one of my colleagues, Daniel Weller, wrote a Scala version of the final solution:

See this content in the original post

You can find his code here -> https://gist.github.com/frehn/661f525ca7361359f69c800203939eb1

Part 2 Table of Contents