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
Dockerfileoption, this will result in the following error when trying to run the application in some casesA 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
1let myInt = 52let myFloat = 3.143let myString = "hello"Lists can be defined with square brackets and elements are separated with semicolons
1let list1 = [2;3;4;5]We can create a new list with a prepended item using :: and a concatenated list with @
1let list2 = 1 :: list1 // [1;2;3;4;5]2let 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
1let mutable changeableValue = 12changeableValue <- 4Note 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
1let refValue = ref 42
3refValue := 14
5let plus1 = !refValue + 1Functions
Functions are defined with the let keyword as well and the parameters are written after the name
1let add x y =2 x + yThe return value in the above function is the result of x + y
You can call the function using the following
1add 1 6 // 7A function can also be partially applied using the following
1let add5 = add 52
3add5 4 // 11Which 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
1let greet () =2 printfn "Hello"3
4greet()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
1let evens list =2 let isEven x = x%2 = 03 List.filter isEven list4
5evens [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
1let evenFilter =2 let isEven x = x%2 = 03 List.filter isEven4
5evenFilter [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
1let sumOfSquares list =2 let square x = x*x3 List.sum (List.map square list)4
5sumOfSquares [1..10]Alternatively, if you want to pass the output from one function into another, you can also use pipes which are done using |>
1let sumOfSquaresPiped list =2 let square x = x*x3
4 list |> List.map square |> List.sum5
6sumOfSquaresPiped [1..10]Or over multiple lines with:
1let sumOfSquaresPiped list =2 let square x = x*x3
4 list5 |> List.map square6 |> List.sum7
8sumOfSquaresPiped [1..10]The square function can also be defined as a lambda or anonymous function using the fun keyword
1let sumOfSquaresLambda list =2 list3 |> List.map (fun x -> x*x)4 |> List.sum5
6sumOfSquaresLambda [1..10]Modules
Functions can be grouped as Modules using the module keyword, with additional functions/variables defined inside of them using the let
1module LocalModule =2 let age = 53 let printName name =4 printfn "My name is %s" name5
6 module Math =7 let add x y =8 x + y9
10LocalModule.printName "John"11printfn "%i" (LocalModule.Math.add 1 3)12printfn "Age is %i" LocalModule.ageModules can also include private properties and methods, these can be defined with the private keyword
1module PrivateStuff =2 let private age = 53 let printAge () =4 printfn "Age is: %i" age5
6// PrivateStuff.age // this will not work7PrivateStuff.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
1module ExternalModule2
3let printName name =4 printfn "My name is %s, - from ExternalModule" name5
6module Math =7 let add x y =8 x + y9
10module MoreMath =11 let subtract x y =12 x - yIf 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
3ExternalModule.printName "Jeff"4ExternalModule.Math.add 1 3If you want to expose the module contents you can do this with the open keyword
1open ExternalModule2
3printName "John"4Math.add 1 5You can also do the same as above to open internal modules
1open ExternalModule.Math2add 1 63
4open MoreMath5subtract 5 1Modules in which submodules/types make use of one another need to have the parent module defined using the rec keyword as well
1module rec RecursiveModuleSwitch 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)
1let patternMatch x =2 match x with3 | "a" -> printfn "x is a"4 | "b" -> printfn "x is b"5 | _ -> printfn "x is something else"6
7patternMatch "a" // x is a8patternMatch "c" // x is something elseThere is also the Some and None are like Nullable wrappers
1let isInputNumber input =2 match input with3 | Some i -> printfn "input is an int: %d" i4 | None -> printfn "input is missing"5
6isInputNumber (Some 5)7
8isInputNumber NoneComplex Data Types
Tuples
Tuples are sets of variables, they are separated by commas
1let twoNums = 1,22let threeStuff = false,"a",2Record Types
Record Types are defined with named fields separated by ;
1type Person = { First:string; Last:string }2let john = {First="John"; Last="Test"} // PersonUnion Types
Union Types have choices separated by `|’
1type Temp =2 | DegreesC of float3 | DegreesF of int4
5let tempC = DegreesC 23.76let tempF = DegreesF 64Types can also be combined recursively such as:
1type Employee =2 | Worker of Person3 | Manager of Employee list4
5let jeff = {First="Jeff"; Last="Smith"}6
7let workerJohn = Worker john8let workerJeff = Worker jeff9
10let 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#
1printfn "int: %i, float: %f, bool: %b" 1 2.0 true2printfn "string: %s, generic: %A" "Hello" [1;3;5]3printfn "tuple: %A, Person: %A, Temp: %A, Employee: %A, Manager: %A" threeStuff john tempC workerJeff johnnyKey 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:
1type Person = { First:string; Last:string }Or a Union type that is a choice between two other types
1type Temp =2 | DegreesC of float3 | DegreesF of intFlow 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:
1match myBool with2| true -> // do stuff3| false -> // do other stuffA switch:
1match myNum with2| 1 -> // some stuff3| 2 -> // some other stuff4| _ -> // other other stuffloops are generally done using recursion like the following
1match myList with2| [] -> // do something for the empty case3| first::rest ->4 // do something with the first element5 // recursively call the functionPattern 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
1type Thing =2 | Cat of age:int * colour:string3 | Potato of isCooked:bool4
5let whatIsIt thing =6 match thing with7 | Cat (age, colour) -> printfn "This is a %s cat, it is %i years old" colour age8 | Potato isCooked ->9 let whatPotato = printfn "This is a %s potato"10 match isCooked with11 | true -> whatPotato "Cooked"12 | false -> whatPotato "Raw"13
14Cat(3,"white")15|> whatIsIt16
17Potato(true)18|> whatIsIt