F# - an introduction for my colleagues

This article was originally published on the Account Technologies Tech Blog.

Having used quite a few programming languages in numerous projects(C#, Powershell, TypeScript, Groovy, and others), my favourite so far is F#. In this article, I want to present to you the unique traits of F# as a pragmatic functional-first language. Hopefully, this article is a mild and concise introduction to F# for people using other languages.

F# is a pragmatic general-purpose programming language. Its core design goals were:

  • succinctness - think of Python-like low-ceremony syntax
  • performance - is on par with C# (they both use the dotnet runtime)
  • robustness - it’s a strongly-typed functional-first language

F# is a multi-paradigm language. It is functional-first but it also supports object-orientated and imperative styles. However, being functional-first makes it quite unique a language comparing to the mainstream.

You can test all the F# examples in the article in the F# interactive tool (fsi). The easiest way to launch it is:

docker run --rm -it mcr.microsoft.com/dotnet/sdk:6.0 dotnet fsi

or just dotnet fsi if you have dotnet SDK already installed.

  1. F# expressions in fsi are multi-line. A double-semicolon ;; is used to indicate the end of an expression.

  2. To quit fsi enter: #q;; or press CTRL+D

  3. Read the docs

What does functional-first mean? Functions are a ubiquitous building block of most F# programs. They can be combined together in numerous ways, passed around as values, and returned from other functions. But what are they? They’re similar to their counterparts in other programming languages. They take input values and either return other values or cause side-effects. Functions are declared using let bindings:

let isEqual a b = a = b
def isEqual(a, b):
  return a == b
bool IsEqual<T>(T a, T b) => a.Equals(b);

Figure 1. A function testing if a is structurally equal to b.

You may be wondering - F# is a strongly-typed language. It means every function must have a signature defining the types of a, b, and the return value. Indeed, an F# IDE or fsi shows something like the below:

val isEqual : a:'a -> b:'a -> bool when 'a : equality

Figure 2. The isEqual function signature

Whoa! You must be kidding me you may say now. What all of that is supposed to mean? Well, the F# compiler does its best trying to guess what the types should be, given the context provided. This is a trait of the F# compiler called type inference. In this particular case the compiler infers that if we test equality then a and b must be of a the same type ('a) and the type must support equality.

We can help the compiler with explicit type annotations. The typical case when we always want to define types is a public API (e.g. in a library). As library authors we leave no room for compiler confusion providing as much context as possible. A fully-annotated function looks like the below:

let isEqual (a: 'a when 'a : equality) (b: 'a when 'a : equality) : bool = a = b

We explicitly declared the types for both parameters as well as the return type. In day to day F# code you hardly use explicit annotations. However, they may be useful in trying to understand why the compiler thinks our code is incorrect.

Interestingly, the compiler seems to assume isEqual can be called with any type as long as the type supports equality. We can prove it with the following invocations:

printfn "%b" (isEqual "F#" "C#")
printfn "%b" (isEqual 1 1)

Figure 3. Invoking the isEqual function with various types.

C# developers will realise the function is actually generic.

What does ‘generic’ mean? - JavaScript or Python developers may ask. Well, let’s imagine a type that is a collection of elements (like Python lists or JS arrays). In statically-typed languages lists are homogenous (i.e. all elements in a list must be of the same type). But if lists must be typed would it mean we have an IntList for integers or a StringList for strings? It would be very impractical. Instead we have a reusable - generic - List<T> where T is a type parameter to be specified by the developer (like List<int> or List<string>). The angle-brackets syntax is used in C#, Java, TypeScript. Other languages, e.g. Scala, use square-brackets. In F# we can just declare:

let emptyList = [] // 'a list - an empty list is considered generic given the context
let list = [1; 2; 3] // int list

And the compiler will infer the type for us. Generic types may come with constraints. In isEqual the type 'a is limited to types supporting equality. Functions types do not support equality. The compiler will fail compiling the following code:

isEqual isEqual isEqual // try to check if isEqual function is equal to itself using itself
// The type '('a -> 'a -> bool)' does not support the 'equality' constraint 
// because it is a function type

So isEqual seems to be generic. But we haven’t consciously declared it as such. This is thanks to another F# compiler’s trait called automatic generalisation. In other words, unless the context proves otherwise, the compiler automatically assumes the function is generic.

Generic functions is a rather foreign concept in dynamically typed languages. Let’s take a look at the following Python example:

print(isEqual(2, "some string"))

Python happily returns False.

printfn "%b" (isEqual 2 "some string")

F#, on the other hand, won’t let you compile the above code stating something like:

error FS0001: This expression was expected to have type
but here has type

The compiler infers, based on the first argument (2 - an integer), that the actual signature of the function in this particular occurrence should be a:int -> b:int -> bool.

This is the fundamental difference between strongly (statically) and weakly (dynamically) typed languages - in the former the types are known on compilation. What in Python could cause a runtime error, in F# is a compilation error (you’d also get that highlighted in the IDE). Many defects can be detected that way much earlier in the development lifecycle (being in the spirit of the fail-fast principle or the shift-left movement).

F# frees you from pondering about the results of the below:

0 == [] // true
0 == {} // false

Let’s now immerse into other functional concepts.

In F# all values are immutable by default. There are no variables in F#. You can declare let bindings to bind a name to a value (or a function) like:

let number = 5
let newNumber = number + 1

Now, we can’t update number. We can only produce a new value. It seems like a huge limitation for people used the mutable-by-default paradigm. However, immutability improves predictability of programs by discouraging mutation of an external state and enabling pure functions.

F# does allow declaring mutable constructs if needed but this is not a part of the functional paradigm.

Algebraic types are types composed from other types. In F# the most common composite data types are records, tuples, and discriminated unions. The first two exist in many other languages and are quite simple.

Tuple is an ordered compound of a fixed number of values of various types:

// Tuple usage
("Applied F#", 150) // string * int
(10, 2, -3) // int * int * int

Records are similar to tuples, however the order of elements is not important as they’re named:

// Record type declaration
type Book = {
  Title: string
  PagesCount: int

// Record usage
let book = { Title = "Applied F#"; PagesCount = 123 }

// Copy-and-update
let newBook = { book with PagesCount = 150 }

Unlike tuples, records need to be explicitly declared before usage. Both tuples and records are product/intersection types, i.e. the Book record has both Title AND PagesCount.

Discriminated unions are less common than tuples/records in the mainstream languages. They are types compound of named cases (so called tagged unions). A common built-in F# type - Option - is a discriminated union declared as follows:

// this is built-in to F# - there is no need for declaring it
type Option<'a> =
 | Some of 'a
 | None

Figure 4. A discriminated union declaration

Discriminated unions (DUs) are sum types, i.e. Option can be either Some value OR None. The following function uses pattern matching to handle various union cases:

let printValue option =
  match option with
  | Some value -> printfn "My value: %A" value
  | None -> printfn "No value!"

printValue (Some 123)
// My value: 123
printValue None
// No value!

Usage of null (and NullReferenceException) in F# is rare. Nulls may happen when interoperating with C# dotnet libraries. In F# however, we need to explicitly express the intent of optionality using the option type (which we’ve already discussed earlier).

In F# most of the code is an expression. It means it returns a value. The compiler watches your expressions closely and makes sure all the branches have the same return type:

let wontCompile x = if x = 5 then "It's 5!" else ()
// error FS0001: All branches of an 'if' expression must return values
// of the same type as the first branch, which here is 'string'.
// This branch returns a value of type 'unit'.

You may wonder what gets returned when there is nothing to be returned (like in C# void). Well, F# has a special type called unit and it looks like an empty tuple - (). The unit return type in a function indicates the function causes some side-effects. In the below example it prints Hello to the standard output:

let printHello () = printfn "Hello" // unit -> unit

Figure 5. A function both taking unit as an argument and returning it.

You may wonder if we really need the input argument? What is the difference?

  • let printHello () = printfn "Hello" - it declares a function that prints “Hello” when called
  • let printHello = printfn "Hello" - it prints “Hello” immediately and binds the result - which is () - to printHello

The pipe operator |> is very common in F#. It is just another way of passing a value to a function. If the function has multiple parameters the value is passed to the left-most parameter. It is quite intuitive:

let add1 x = x + 1
let multiplyBy5 x = x * 5
let printIt x = printfn "The number is %i" x

10 |> add1 |> multiplyBy5 |> printIt
// The number is 55

In the above example we pass 10 to add1, then we pass the result of that call to multiplyBy5. Then we pass the result to printIt. We can also write the above expression without pipes but we quickly drown in parentheses and it’s harder to read:

printIt (multiplyBy5 (add1 10))

F# has a robust built-in library of functions operating on collections. The functions are typically joined with |> to form more complex pipelines:

[2; 3; 5; 7; 11; 13; 17; 19; 23]
  |> List.chunkBySize 3
  |> List.last
// [17; 19; 23]

In the rich world of F# operators there is also a function composition operator i.e. >>. We can use it to compose two or more functions into one.

let processInt = add1 >> multiplyBy5 >> printIt

processInt 1000
// The number is 5005

To use the operator the output type of the left function and the input type of the right function must match.

Application is a fancy name for the act of calling a function. Partial application? How can we possibly call a function partially? Usually we can’t. Let’s once again take a look at a decluttered version of isEqual signature:

'a -> 'a -> bool

Figure 6. The isEqual function signature limited to types

How should we read it? We know the function takes two arguments of type 'a and returns a bool. But it doesn’t really feel like when looking at the signature, does it? What is this -> symbol? It defines a function type of one argument and one return value. On the left it specifies the argument type and on the right the return type. If we apply that thinking to isEqual signature it becomes clear that the function takes one argument of 'a and returns the type of 'a -> bool which is a function type.

'a -> ('a -> bool)

Figure 7. The isEqual function signature with redundant parentheses to emphasize the meaning of the -> operator.

Summing up, it looks like the isEqual function takes one argument and returns another function that also takes one argument.

Stop it! I have just seen it takes two arguments and returns a bool.

Well, out of the sudden calling a function partially makes more sense - if it takes one argument means we can call it with just one argument and we should get a new function that only expects the remaining argument:

let isEqual a b = a = b
let isEqual5 = isEqual 5 // int -> bool

You’ll notice two things - the isEqual5 is indeed a new function and now the compiler inferred its signature to be int -> bool. This is because the provided context (the integer 5) caused the all the possible types to collapse to just int. We can invoke the created function (fully apply it) passing the remaining argument like:

printfn "%b" (isEqual5 5)

Most of F# functions are automatically-curried, i.e. declared in a way partial application is available out of the box. The curried form means that a function of multiple arguments gets transformed into a chain of functions each taking a single argument.

I tried to cover as many fundamental features of F# as I could in a short blog post. It is obviously not all of it. However, I hope I made it interesting enough for you to further explore the language.