Introduction to Functional Programming in F# – Part 9

Introduction

In this post we are going to see how we can improve the readability of our code by increasing our usage of domain concepts and reducing our use of primitives.

Setting Up

We are going to use the code from Part 1.

Solving the Problem

This is where we left the code from the first post in this series:

type RegisteredCustomer = {
    Id : string
}

type UnregisteredCustomer = {
    Id : string
}

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

let calculateTotal customer spend = // Customer -> decimal -> decimal
    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

It's nice but we still have some primitives where we should have domain concepts (Spend and Total). I would like the signature of the calculateTotal function to be Customer -> Spend -> Total. The easiest way to achieve this is to use type abbreviations. Add the following below the Customer type definition:

type Spend = decimal
type Total = decimal

We have two approaches we can use our new type abbreviations and get the type signature we need: Explicitly creating and implementing a type signature (which we did in Part 6) or using explicit types for the input/output parameter. Type signature first:

type CalcualteTotal = Customer -> Spend -> Total

let calculateTotal : CalculateTotal =
    fun customer spend ->
        let discount = 
            match customer with
            | EligibleCustomer _ when spend >= 100.0M -> spend * 0.1M
            | _ -> 0.0M
        spend - discount

Followed by explicit parameters:

let calculateTotal (customer:Customer) (spend:Spend) : Total =
    let discount = 
        match customer with
        | EligibleCustomer _ when spend >= 100.0M -> spend * 0.1M
        | _ -> 0.0M
    spend - discount

Either approach gives us the signature we want. I have no preference for the style as both are useful to know, so we will use the explicit parameters for the rest of this post.

There is a potential problem with type abbreviations; As long as the underlying type matches, I can use anything, not just Spend. Imagine that you had a function that takes three string arguments. There is nothing stopping you supplying the wrong value to a parameter.

type CustomerName = {
    First : string
    Middle : string
    Last : string
}

let createName first middle last =
    { First = first; Middle = middle; Last = last }

let name = createName "????" "Russell" "Ian"

Of course we can write tests to prevent this from happening but that is additional work. Thankfully there is a way that we can stop this in F# - The single case discriminated union!

type Spend = Spend of decimal

It is convention to write it this way but it is also:

type Spend =
| Spend of decimal

You will notice that the calculateTotal function now has errors. We can fix that by deconstructing the Spend parameter value in the function:

let calculateTotal (customer:Customer) (spend:Spend) : Total =
    let (Spend value) = spend
    let discount = 
        match customer with
        | EligibleCustomer _ when value >= 100.0M -> value * 0.1M
        | _ -> 0.0M
    value - discount

If you replace all of your primitives and type abbreviations with single case discriminated unions, you cannot supply the wrong parameter as the compiler will stop you.

The next improvement is to restrict the range of values that Spend can accept since very few domain values will be unbounded. We will restrict Spend to between 0 and 1000. To do this we are going to add a ValidationError type, prevent the direct use of the Spend constructor and add a new module to handle the domain rules and the creation of the instance.

type ValidationError = 
| InputOutOfRange of string

type Spend = private Spend of decimal

module Spend =
    let create input =
        if input >= 0.0M || input < 1000.0M then
            Ok (Spend input)
        else
            Error (InputOutOfRange "You can only spend between 0 and 1000")
    let value (Spend input) = input

The name of the module must match the name of the type. We need to amke some changes to get the code to compile. Firstly we change the first line of the calculateTotal function to use the new value function:

let calculateTotal (customer:Customer) (spend:Spend) : Total =
    let value = Spend.value spend
    let discount = 
        match customer with
        | EligibleCustomer _ when value >= 100.0M -> value * 0.1M
        | _ -> 0.0M
    value - discount

We also need to fix the asserts. To do this, we are going to add a new helper function:

let doCalculateTotal name amount =
    match Spend.create amount with
    | Ok spend -> calculateTotal name spend
    | Error ex -> failwith (sprintf "%A" ex)

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

As a piece of homework, try to replace the validation we did in Part 8 with what you have learnt in this post.

The final change that we can make is to move the discount rate from the calculateTotal function to be with the Customer type. The primary reason for doing this would be if we needed to use the discount in anther function:

type Customer =
    | EligibleCustomer of RegisteredCustomer
    | RegisteredCustomer of RegisteredCustomer
    | Guest of UnregisteredCustomer
    with 
        member this.Discount =
            match this with
            | EligibleCustomer _ -> 0.1M
            | _ -> 0.0M

This also allows us to simplify the calculateTotal function:

let calculateTotal (customer:Customer) (spend:Spend) : Total =
    let value = Spend.value spend
    let discount = if value >= 100.0M then value * customer.Discount else 0.0M
    value - discount

Whilst this looks nice, it has broken the link between the customer type and the spend. If you remember, the rule is a 10% discount if an eligible customer spends 100.0 or more. Let's have another go:

type Customer =
    | EligibleCustomer of RegisteredCustomer
    | RegisteredCustomer of RegisteredCustomer
    | Guest of UnregisteredCustomer
    with 
        member this.CalculateDiscount(spend:Spend) =
            match this with
            | EligibleCustomer _ -> 
                let value = Spend.value spend
                if value >= 100.0 then 0.1M else 0.0M
            | _ -> 0.0M

You will probably need to move some of your types to before the Customer declaration in the file.

We now need to modify our calculateTotal function to use our new function:

let calculateTotal (customer:Customer) (spend:Spend) : Total =
    let value = Spend.value spend
    let discount = value * customer.CalculateDiscount spend
    value - discount

To run this code, you will need to load all of it back into FSI. Your tests should still pass.

Is this approach better? It's hard to say but at least you are now aware of some of the options.

Final Code

type RegisteredCustomer = {
    Id : string
}

type UnregisteredCustomer = {
    Id : string
}

type ValidationError = 
| InputOutOfRange of string

type Spend = private Spend of decimal

module Spend =
    let create input =
        if input >= 0.0M || input < 1000.0M then
            Ok (Spend input)
        else
            Error (InputOutOfRange "You can only spend between 0 and 1000")
    let value (Spend input) = input

type Total = decimal

type Customer =
    | EligibleCustomer of RegisteredCustomer
    | RegisteredCustomer of RegisteredCustomer
    | Guest of UnregisteredCustomer
    with 
        member this.CalculateDiscount(spend:Spend) =
            match this with
            | EligibleCustomer _ -> 
                let value = Spend.value spend
                if value >= 100.0 then 0.1M else 0.0M
            | _ -> 0.0M

let calculateTotal (customer:Customer) (spend:Spend) : Total =
    let value = Spend.value spend
    let discount = value * customer.CalculateDiscount spend
    value - discount

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

let doCalculateTotal name amount =
    match Spend.create amount with
    | Ok spend -> calculateTotal name spend
    | Error ex -> failwith (sprintf "%A" ex)

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

Conclusion

In this post we have learnt about the single case discriminated union to enable us the restrict the range of data that we support for a parameter and that we can add helper functions/properties to discriminated unions.

In the next post we will look at object programming.

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

 

Blog 5/18/22

Introduction to Functional Programming in F#

Dive into functional programming with F# in our introductory series. Learn how to solve real business problems using F#'s functional programming features. This first part covers setting up your environment, basic F# syntax, and implementing a simple use case. Perfect for developers looking to enhance their skills in functional programming.

Blog 9/15/22

Introduction to Functional Programming in F# – Part 3

Dive into F# data structures and pattern matching. Simplify code and enhance functionality with these powerful features.

Blog 9/13/22

Introduction to Functional Programming in F# – Part 2

Explore functions, types, and modules in F#. Enhance your skills with practical examples and insights in this detailed guide.

Blog 8/8/23

Introduction to Functional Programming in F# – Part 12

Explore reflection and meta-programming in F#. Learn how to dynamically manipulate code and enhance flexibility with advanced techniques.

Blog 10/11/22

Introduction to Functional Programming in F# – Part 5

Master F# asynchronous workflows and parallelism. Enhance application performance with advanced functional programming techniques.

Blog 3/22/23

Introduction to Functional Programming in F# – Part 8

Discover Units of Measure and Type Providers in F#. Enhance data management and type safety in your applications with these powerful tools.

Blog 12/22/22

Introduction to Functional Programming in F# – Part 7

Explore LINQ and query expressions in F#. Simplify data manipulation and enhance your functional programming skills with this guide.

Blog 5/17/23

Introduction to Functional Programming in F# – Part 10

Discover Agents and Mailboxes in F#. Build responsive applications using these powerful concurrency tools in functional programming.

Blog 10/1/22

Introduction to Functional Programming in F# – Part 4

Unlock F# collections and pipelines. Manage data efficiently and streamline your functional programming workflow with these powerful tools.

Blog 7/12/23

Introduction to Functional Programming in F# – Part 11

Learn type inference and generic functions in F#. Boost efficiency and flexibility in your code with these essential programming concepts.

Blog 12/3/21

Using Discriminated Union Labelled Fields

A few weeks ago, I re-discovered labelled fields in discriminated unions. Despite the fact that they look like tuples, they are not.

Blog 12/22/22

Introduction to Functional Programming in F# – Part 6

Learn error handling in F# with option types. Improve code reliability using F#'s powerful error-handling techniques.

Blog 3/11/21

Introduction to Web Programming in F# with Giraffe – Part 2

In this series we are investigating web programming with Giraffe and the Giraffe View Engine plus a few other useful F# libraries.

Blog 3/10/21

Introduction to Web Programming in F# with Giraffe – Part 1

In this series we are investigating web programming with Giraffe and the Giraffe View Engine plus a few other useful F# libraries.

Blog 3/12/21

Introduction to Web Programming in F# with Giraffe – Part 3

In this series we are investigating web programming with Giraffe and the Giraffe View Engine plus a few other useful F# libraries.

Blog 8/7/20

Understanding F# Type Aliases

In this post, we discuss the difference between F# types and aliases that from a glance may appear to be the same thing.

Blog 5/20/22

My Weekly Shutdown Routine

Discover my weekly shutdown routine to enhance productivity and start each week fresh. Learn effective techniques for reflection and organization.

Blog 7/21/20

Understanding F# applicatives and custom operators

In this post, Jonathan Channon, a newcomer to F#, discusses how he learnt about a slightly more advanced functional concept — Applicatives.

Blog 11/30/22

Introduction to Partial Function Application in F#

Partial Function Application is one of the core functional programming concepts that everyone should understand as it is widely used in most F# codebases.In this post I will introduce you to the grace and power of partial application. We will start with tupled arguments that most devs will recognise and then move onto curried arguments that allow us to use partial application.

Blog 7/22/24

So You are Building an AI Assistant?

So you are building an AI assistant for the business? This is a popular topic in the companies these days. Everybody seems to be doing that. While running AI Research in the last months, I have discovered that many companies in the USA and Europe are building some sort of AI assistant these days, mostly around enterprise workflow automation and knowledge bases. There are common patterns in how such projects work most of the time. So let me tell you a story...

Bleiben Sie mit dem TIMETOACT GROUP Newsletter auf dem Laufenden!