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.

Feature: Applying a discount
Scenario: Eligible Registered Customers get 10% discount when they spend £100 or more

Given the following Registered Customers
|Customer Id|Is Eligible|
|John       |true       |
|Mary       |true       |
|Richard    |false      |
When <Customer Id> spends <Spend>
Then their order total will be <Total>
Examples:
|Customer Id|   Spend|   Total|
|Mary       |   99.00|   99.00|
|John       |  100.00|   90.00|
|Richard    |  100.00|  100.00|
|Sarah      |  100.00|  100.00|

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:

type Customer = {
    Id : string
    IsEligible : bool
    IsRegistered : bool
}

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

let fred = { Id = "Fred"; IsEligible = true; IsRegistered = true }

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).

let calculateTotal (customer:Customer) (spend:decimal) : decimal =
    let discount = if customer.IsRegistered && customer.IsEligible && spend >= 100.0M then (spend * 0.1M) else 0.0M   
    let total = spend - discount
    total

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:

let calculateTotal customer spend =
    let discount = if customer.IsRegistered && customer.IsEligible && spend >= 100.0M then (spend * 0.1M) else 0.0M   
    spend - discount

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:

let john = { Id = "John"; IsEligible = true; IsRegistered = true }

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.

let assertJohn = (calculateTotal john 100.0M = 90.0M)

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

val assertJohn : bool = true

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

let john = { Id = "John"; IsEligible = true; IsRegistered = true }
let mary = { Id = "Mary"; IsEligible = true; IsRegistered = true }
let richard = { Id = "Richard"; IsEligible = false; IsRegistered = true }
let sarah = { Id = "Sarah"; IsEligible = false; IsRegistered = false }

let assertJohn = calculateTotal john 100.0M = 90.0M
let assertMary = calculateTotal mary 99.0M = 99.0M
let assertRichard = calculateTotal richard 100.0M = 100.0M
let assertSarah = calculateTotal sarah 100.0M = 100.0M

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

val assertJohn : bool = true
val assertMary : bool = true
val assertRichard : bool = true
val assertSarah : bool = true

Your code should now look like this:

type Customer = {
    Id : string
    IsEligible : bool
    IsRegistered : bool
}

let calculateTotal customer spend =
    let discount = if customer.IsRegistered && customer.IsEligible && spend >= 100.0M then (spend * 0.1M) else 0.0M   
    spend - discount

let john = { Id = "John"; IsEligible = true; IsRegistered = true }
let mary = { Id = "Mary"; IsEligible = true; IsRegistered = true }
let richard = { Id = "Richard"; IsEligible = false; IsRegistered = true }
let sarah = { Id = "Sarah"; IsEligible = false; IsRegistered = false }

let assertJohn = calculateTotal john 100.0M = 90.0M
let assertMary = calculateTotal mary 99.0M = 99.0M
let assertRichard = calculateTotal richard 100.0M = 100.0M
let assertSarah = calculateTotal sarah 100.0M = 100.0M

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.

type RegisteredCustomer = {
    Id : string
    IsEligible : bool
}

type UnregisteredCustomer = {
    Id : string
}

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:

type Customer =
    | RegisteredCustomer of RegisteredCustomer
    | Guest of UnregisteredCustomer

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:

let sarah = Guest { Id = "Sarah" } // Guest of UnregisteredCustomer

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

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

let john = RegisteredCustomer { Id = "John"; IsEligible = true }
let mary = RegisteredCustomer { Id = "Mary"; IsEligible = true }
let richard = RegisteredCustomer { Id = "Richard"; IsEligible = false }

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:

let calculateTotal customer spend =
    let discount = 
        match customer with
        | RegisteredCustomer c -> if c.IsEligible && spend >= 100.0M then (spend * 0.1M) else 0.0M
        | Guest _ -> 0.0M
    spend - discount

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:

let calculateTotal customer spend =
    let discount = 
        match customer with
        | RegisteredCustomer c when c.IsEligible && spend >= 100.0M -> spend * 0.1M
        | RegisteredCustomer _ -> 0.0M
        | Guest _ -> 0.0M
    spend - discount

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

let calculateTotal customer spend =
    let discount = 
        match customer with
        | RegisteredCustomer c when c.IsEligible && spend >= 100.0M -> spend * 0.1M
        | _ -> 0.0M
    spend - discount

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.

type RegisteredCustomer = {
    Id : string
}

type UnregisteredCustomer = {
    Id : string
}

type Customer =
    | EligibleRegisteredCustomer of RegisteredCustomer
    | RegisteredCustomer of RegisteredCustomer
    | Guest of UnregisteredCustomer
We need to make a change to our function.

let calculateTotal customer spend =
    let discount = 
        match customer with
        | EligibleRegisteredCustomer _ when spend >= 100.0M -> spend * 0.1M
        | _ -> 0.0M
    spend - discount

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.

let john = EligibleRegisteredCustomer { Id = "John" }
let mary = EligibleRegisteredCustomer { Id = "Mary" }
Run your code in FSI to check all is still OK.

The state of our code after all of our improvements is:

type RegisteredCustomer = {
    Id : string
}

type UnregisteredCustomer = {
    Id : string
}

type Customer =
    | EligibleRegisteredCustomer of RegisteredCustomer
    | RegisteredCustomer of RegisteredCustomer
    | Guest of UnregisteredCustomer

let calculateTotal customer spend =
    let discount = 
        match customer with
        | EligibleRegisteredCustomer _ when spend >= 100.0M -> spend * 0.1M
        | _ -> 0.0M
    spend - discount

let john = EligibleRegisteredCustomer { Id = "John" }
let mary = EligibleRegisteredCustomer { Id = "Mary" }
let richard = RegisteredCustomer { Id = "Richard" }
let sarah = Guest { Id = "Sarah" }

let assertJohn = calculateTotal john 100.0M = 90.0M
let assertMary = calculateTotal mary 99.0M = 99.0M
let assertRichard = calculateTotal richard 100.0M = 100.0M
let assertSarah = calculateTotal sarah 100.0M = 100.0M

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:

sealed trait Customer

case class RegisteredCustomer(id : String) extends Customer
case class EligibleRegisteredCustomer(id : String) extends Customer
case class Guest(id: String) extends Customer

def calculateTotal(customer: Customer)(spend: Double) = {
    val discount = customer match {
        case EligibleRegisteredCustomer(_) if spend >= 100.0 => spend * 0.1
        case _ => 0.0
    }
    spend - discount
}

val john = EligibleRegisteredCustomer("John")
val assertJohn = (calculateTotal (john) (100.0)) == 90.0
Zurück
Zurück

Introduction to Functional Programming in F# – Part 2