Introduction to F#

Basic Introduction to the F# Programming Language

Updated: 03 September 2023

Mostly based on the content here

Foreword: When creating a new F# project from Visual Studio do not check the Dockerfile option, this will result in the following error when trying to run the application in some cases A function labeled with the 'EntryPointAttribute' attribute must be the last declaration in the last file in the compilation sequence.

F# is a functional language that runs in the .NET Core ecosystem. It’s a weird language.

Install and Run

Before you can get started with F# you will need to install it, there are a few different options available depending on your choice of operating system and editor, these instructions can be found here

There are a few different styles that we can use to run F#, the simplest would be an F# script, which uses the .fsx extension

You can run F# code using Visual Studio and starting a new F# project, console app or even just by creating a new F# script file, if running a .fsx file you can highlight the code you want to run and clicking alt+enter

Alternatively you can run F# in Visual Studio Code using the same method as above with theIonide F# Language Support extension

Syntax

F# is whitespace sensitive and uses indentation to denote code blocks

F# makes use of implicit typing however you can explicitly state types as well

Variables

Variables are immutable by default and are defined using the let keyword.

Also, ideally they don’t vary

1
let myInt = 5
2
let myFloat = 3.14
3
let myString = "hello"

Lists can be defined with square brackets and elements are separated with semicolons

1
let list1 = [2;3;4;5]

We can create a new list with a prepended item using :: and a concatenated list with @

1
let list2 = 1 :: list1 // [1;2;3;4;5]
2
let list3 = [0;1] @ list1 //[0;1;2;3;4;5]

Mutable and Reference Values

You can create Mutable variables which would allow the value to be changed, this can be done using the mutable keyword as follows

1
let mutable changeableValue = 1
2
changeableValue <- 4

Note that in the above case the <- operator is used to assign values to a mutable varia

Reference values are sort of like wrappers around mutable value. Defining them makes use of the ref keyword, modifying them makes use of the:= operator to assign values and ! to access values

1
let refValue = ref 4
2
3
refValue := 1
4
5
let plus1 = !refValue + 1

Functions

Functions are defined with the let keyword as well and the parameters are written after the name

1
let add x y =
2
x + y

The return value in the above function is the result of x + y

You can call the function using the following

1
add 1 6 // 7

A function can also be partially applied using the following

1
let add5 = add 5
2
3
add5 4 // 11

Which sets x as 5 and returns a function for 5 + y

If a function does not have any input parameters, it should be defined using () to indicate that it is a function and must also be used with the () to actually apply the function and not just reference the variable

1
let greet () =
2
printfn "Hello"
3
4
greet()

A function that returns only even numbers can be defined using the following function within a function and the List.filter function which takes in a predicate and a list as inputs

1
let evens list =
2
let isEven x = x%2 = 0
3
List.filter isEven list
4
5
evens [1;2;3;4;5;6;7;8] // [2;4;6;8]

We can write the above in the following form as well which returns a function that will filter out the even values in a list

1
let evenFilter =
2
let isEven x = x%2 = 0
3
List.filter isEven
4
5
evenFilter [1;2;3;4;5;6;7;8] // [2;4;6;8]

Parenthesis can be used to specify the order in which functions should be applied

1
let sumOfSquares list =
2
let square x = x*x
3
List.sum (List.map square list)
4
5
sumOfSquares [1..10]

Alternatively, if you want to pass the output from one function into another, you can also use pipes which are done using |>

1
let sumOfSquaresPiped list =
2
let square x = x*x
3
4
list |> List.map square |> List.sum
5
6
sumOfSquaresPiped [1..10]

Or over multiple lines with:

1
let sumOfSquaresPiped list =
2
let square x = x*x
3
4
list
5
|> List.map square
6
|> List.sum
7
8
sumOfSquaresPiped [1..10]

The square function can also be defined as a lambda or anonymous function using the fun keyword

1
let sumOfSquaresLambda list =
2
list
3
|> List.map (fun x -> x*x)
4
|> List.sum
5
6
sumOfSquaresLambda [1..10]

Modules

Functions can be grouped as Modules using the module keyword, with additional functions/variables defined inside of them using the let

1
module LocalModule =
2
let age = 5
3
let printName name =
4
printfn "My name is %s" name
5
6
module Math =
7
let add x y =
8
x + y
9
10
LocalModule.printName "John"
11
printfn "%i" (LocalModule.Math.add 1 3)
12
printfn "Age is %i" LocalModule.age

Modules can also include private properties and methods, these can be defined with the private keyword

1
module PrivateStuff =
2
let private age = 5
3
let printAge () =
4
printfn "Age is: %i" age
5
6
// PrivateStuff.age // this will not work
7
PrivateStuff.printAge()

You can define a module in a different file and can then import this into another file using the open keyword. Note that there needs to be a top-level module which does not make use of the = sign, but other internal modules do

1
module ExternalModule
2
3
let printName name =
4
printfn "My name is %s, - from ExternalModule" name
5
6
module Math =
7
let add x y =
8
x + y
9
10
module MoreMath =
11
let subtract x y =
12
x - y

If using a script, you will need to first load the module to make the contents available, you can then use the values from the Module using the name as an accessor. This will now essentially function as if the

1
#load "ExternalModule.fs"
2
3
ExternalModule.printName "Jeff"
4
ExternalModule.Math.add 1 3

If you want to expose the module contents you can do this with the open keyword

1
open ExternalModule
2
3
printName "John"
4
Math.add 1 5

You can also do the same as above to open internal modules

1
open ExternalModule.Math
2
add 1 6
3
4
open MoreMath
5
subtract 5 1

Modules in which submodules/types make use of one another need to have the parent module defined using the rec keyword as well

1
module rec RecursiveModule

Switch Statements

Switch statements can be used with the match ... with keyword, | to separate comparators, and -> for the resulting statement. An _ is used to match anything (default)

1
let patternMatch x =
2
match x with
3
| "a" -> printfn "x is a"
4
| "b" -> printfn "x is b"
5
| _ -> printfn "x is something else"
6
7
patternMatch "a" // x is a
8
patternMatch "c" // x is something else

There is also the Some and None are like Nullable wrappers

1
let isInputNumber input =
2
match input with
3
| Some i -> printfn "input is an int: %d" i
4
| None -> printfn "input is missing"
5
6
isInputNumber (Some 5)
7
8
isInputNumber None

Complex Data Types

Tuples

Tuples are sets of variables, they are separated by commas

1
let twoNums = 1,2
2
let threeStuff = false,"a",2

Record Types

Record Types are defined with named fields separated by ;

1
type Person = { First:string; Last:string }
2
let john = {First="John"; Last="Test"} // Person

Union Types

Union Types have choices separated by `|’

1
type Temp =
2
| DegreesC of float
3
| DegreesF of int
4
5
let tempC = DegreesC 23.7
6
let tempF = DegreesF 64

Types can also be combined recursively such as:

1
type Employee =
2
| Worker of Person
3
| Manager of Employee list
4
5
let jeff = {First="Jeff"; Last="Smith"}
6
7
let workerJohn = Worker john
8
let workerJeff = Worker jeff
9
10
let johnny = Manager [workerJohn;workerJeff]

Printing

Printing can be done using the printf and printfn functions which are similar to Console.Write and Console.WriteLine functions in C#

1
printfn "int: %i, float: %f, bool: %b" 1 2.0 true
2
printfn "string: %s, generic: %A" "Hello" [1;3;5]
3
printfn "tuple: %A, Person: %A, Temp: %A, Employee: %A, Manager: %A" threeStuff john tempC workerJeff johnny

Key Concepts

F# has four key concepts that are used when aproaching problems

Function Oriented

F# is a functional language and functions are first-class entities and can be used as any other value/variable

This enables you to write code using functional composition in order to build complex functions out of basic functions

Expressions over Statements

F# prefers to make use of expressions instead of statements. Variables tend to be declared at the same time they are assigned and do not need to be ‘set-up’ for use, such as in the typical case of an if-else statement

Algebraic Types

Types are based on the concept of algebraic types where compound types are built out of their composition with other types

In the case of the below you would be creating a Product type that is a combination of two strings, for example:

1
type Person = { First:string; Last:string }

Or a Union type that is a choice between two other types

1
type Temp =
2
| DegreesC of float
3
| DegreesF of int

Flow Control with Matching

Instead of making use of if ... else, switch ... case, for, while among others like most languages, F# uses patttern-matchin using match ... with to handle much of the functionality of the above

An example of an if-then can be:

1
match myBool with
2
| true -> // do stuff
3
| false -> // do other stuff

A switch:

1
match myNum with
2
| 1 -> // some stuff
3
| 2 -> // some other stuff
4
| _ -> // other other stuff

loops are generally done using recursion like the following

1
match myList with
2
| [] -> // do something for the empty case
3
| first::rest ->
4
// do something with the first element
5
// recursively call the function

Pattern Matching with Union Types

Union Types can also be used in matching and a function based on them can be used to correctly handle and apply the arguments, provided the return type is consistent

1
type Thing =
2
| Cat of age:int * colour:string
3
| Potato of isCooked:bool
4
5
let whatIsIt thing =
6
match thing with
7
| Cat (age, colour) -> printfn "This is a %s cat, it is %i years old" colour age
8
| Potato isCooked ->
9
let whatPotato = printfn "This is a %s potato"
10
match isCooked with
11
| true -> whatPotato "Cooked"
12
| false -> whatPotato "Raw"
13
14
Cat(3,"white")
15
|> whatIsIt
16
17
Potato(true)
18
|> whatIsIt