This article was originally published on the Account Technologies Tech Blog.
Motivation ¶
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.
What is F# ¶
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.
Try it yourself ¶
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.
A few tips ¶
-
F# expressions in
fsi
are multi-line. A double-semicolon;;
is used to indicate the end of an expression. -
To quit
fsi
enter:#q;;
or pressCTRL+D
Functional-first ¶
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:
F# ¶
let isEqual a b = a = b
Python ¶
def isEqual(a, b):
return a == b
C# ¶
bool IsEqual<T>(T a, T b) => a.Equals(b);
Figure 1. A function testing if a
is structurally equal to b
.
Type inference ¶
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 the same type ('a
) and the type must support equality.
What if the compiler gets lost? ¶
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.
Automatic generalisation ¶
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.
A short note on generic types ¶
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.
Static for the win ¶
Generic functions is a rather foreign concept in dynamically typed languages. Let’s take a look at the following Python example:
Python ¶
print(isEqual(2, "some string"))
False
Python happily returns False
.
F# ¶
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
'int'
but here has type
'string'
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).
Bonus ¶
F# frees you from pondering about the results of the below:
JavaScript ¶
0 == [] // true
0 == {} // false
Let’s now immerse into other functional concepts.
Immutability ¶
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 data types ¶
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.
Tuples ¶
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 ¶
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 ¶
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!
Forbidden nulls ¶
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).
Expressions everywhere ¶
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 calledlet printHello = printfn "Hello"
- it prints “Hello” immediately and binds the result - which is()
- toprintHello
Pipelines and composition ¶
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.
Partial application ¶
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.
Wrapping up ¶
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.