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
Install F# (Installing the dotnet core SDK will install F#)
Install VSCode with the ionide extension (VS2019 or JetBrains Rider will work as well)
Open VSCode and open a blank folder to store your code.
Add a new file and name it first.fsx
Type 1 = 1 into the file.
Highlight the code and press ALT + ENTER
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.
When <Customer Id> spends <Spend>
Then their order total will be <Total>
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:
To create an instance of a customer we would write the following below the type definition:
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).
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:
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:
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.
What you should see after running the test in FSI is the following:
Add in the other users and test cases from the specification.
Highlight the new code and press ALT + ENTER. You should see the following in FSI.
Your code should now look like this:
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.
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:
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:
Look at how the definition in the DU compares to the binding.
Now let's make the required changes to the RegisteredCustomers:
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:
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:
We can simplify the last two matches using a wildcard like this:
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.
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.
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:
You can find his code here -> https://gist.github.com/frehn/661f525ca7361359f69c800203939eb1