United Kingdom: +44 (0)208 088 8978

Why we love SAFE Stack – F#

Today we go over just a few of the reasons why we love F#!

We're hiring Software Developers

Click here to find out more

The F in SAFE

Today's post concerns the "F" in SAFE - which is for F#; a functional-first .NET programming language! We love F# for a whole bunch of reasons - power, simplicity, and its functional yet pragmatic approach to helping us solve problems.

It is worth pointing out that we also run F# on the front end, by using a tool called Fable - which will be discussed in a later post - which transpiles F# to Javascript. This means we are not limited to back-end work; we can write F# code for the front-end and even code which is shared between front-end and back-end. This means we can take advantage of F#'s powerful features and compile-time checking in all areas of our codebase. As JS users will probably realise, this is a great advantage as it allows us to catch many potential bugs at compile time rather than waiting for something to break at runtime.

Functional first

The design of the F# language encourages writing functional code. One of the fundamental design decisions in F# is that by default data cannot be changed once set, and it has a host of knock-on effects. For example here we give x a value of 10:

let x = 10

... from here on out we're not worried that something might change x - it is easy to reason about because we know it will always be 10.

This raises questions for programmers new to functional programming. They might ask something like "what if I need to sum up some values?" - which would typically involve updating a value in a loop. Well, instead of a loop where some value is changed (mutated) on each iteration, we have functions to achieve pretty much any of the effects that you would usually use a loop for.

For example, to sum:

let total = List.sum [1;2;3;4]

Okay, but that's so simple! What about something more complex?

type Player = { Name: string; Score: int; Team: string }
let players = [
    { Name = "Anna"; Score = 10; Team = "Red" }
    { Name = "Ben"; Score = 20; Team = "Red"}
    { Name = "Claire"; Score = 15; Team = "Blue" }
    { Name = "Derek"; Score = 10; Team = "Blue" }
]
let redScore = players |> List.filter (fun p -> p.Team = "Red") |> List.sumBy _.Score
let blueScore = players |> List.filter (fun p -> p.Team = "Blue") |> List.sumBy _.Score

To find each team's score above, we pass the players into List.filter to filter by team name, then we pass the result into List.sumBy. Simple!

The forward pipe operator, |>, feeds a value from the left hand side into a function on the right hand side. It is similar to function composition (>> in F#) which is a core part of functional programming, but I think it is a bit more friendly to use for people who are newer to functional programming. The forward pipe operator is well suited to creating pipelines of operations where each step feeds into the next step. Chaining together transformations like this is a very natural way of problem solving.

Notice that we actually passed a function in as a parameter to the List.filter function: (fun p -> p.Team = "Red") - which means that List.filter is a so-called "higher-order function" since it accepts a function as a parameter. Use of higher-order functions is a key characteristic of functional programming languages such as F#.

It is important to note that F# is not forcing you to do this. Sometimes it is better to write mutable code in an imperative style, for instance on very hot code paths, and this can easily be achieved in F# - it is just less idiomatic.

Features of F#

Let's take a look at a slightly longer example which showcases some of F#'s features.

// First of all we define types - these are not classes, they don't have any methods!
type Item = {
    Name: string
    Price: decimal
}

type Discount =
    | Percentage of decimal
    | Flat of decimal

type Order = {
    Items: Item list
    Discount: Discount option
}

// Next, some behaviour
let price order =
    let undiscountedPrice = order.Items |> List.map _.Price |> List.sum
    match order.Discount with
    | Some (Flat amount) -> undiscountedPrice - amount
    | Some (Percentage pct) -> undiscountedPrice * (1.0M - pct)
    | None -> undiscountedPrice

// Finally, example data, then we feed it into the `price` function
let order = {
    Items = [
        { Name = "Apple"; Price = 0.8M }
        { Name = "Banana"; Price = 0.5M }
        { Name = "Orange"; Price = 0.7M }
        { Name = "Watermelon"; Price = 3.0M }
    ]
    Discount = Some (Percentage 0.3M)
}
printfn $"The final price of the order is £%M{price order}."

Let's highlight some of the features of the above code:

Starting from the top, types are the building blocks of any F# program, and we create a lot of them, so it makes sense that they are simple to create, but they are very powerful.

There are two kinds which we prefer:

  • Records, such as Item above, which are great for storing related values together.
  • Discriminated Unions - DUs, sometimes known as Tagged Unions - such as Discount, are like super-charged enums, where each case can hold data which can be of different types.

F# avoids NULL, and records in conjunction with DUs create an algebraic type system. All of this together make F#'s types well suited to accurately modelling domains in a way where we can minimise or totally eliminate the ability to represent invalid state.

Just imagine how much defensive code you don't have to write when you know that the state cannot be invalid!

Records and DUs are particularly nice to pattern match against. Pattern matching, denoted by the keyword match, allows your code to be very expressive and clear, and the F# compiler will let you know if haven't fully accounted for all possibilites. This means that we can write complex combinations of conditions to match against, written in a way which is easy to follow, but without being concerned that we might have missed some corner-case.

F#'s syntax is so light and clean that I would wager some readers may not have realised that price is a function. We write a lot of functions so why add useless punctuation unless it is needed? There are no type specifications needed since the compiler can easily infer which types are being referred to. No noisy syntax - parentheses or curly brackets - just clean and concise but without being cryptic.

Other things we love about F#

To keep this post a reasonable length we have just talked about our most favourite bits at a high level, but there are a lot more worth a look.

Some examples include:

If you haven't already, why not pick up F# and give it a go, we're confident that there's something interesting for everyone!