F# Training
F# Training
F# Training
  • Presentation
  • Fundamentals
    • Introduction
    • Syntax
      • Bases
      • Functions
      • Rules
      • Exceptions
    • First concepts
    • πŸ”Quiz
  • Functions
    • Signature
    • Concepts
    • Syntax
    • Standard functions
    • Operators
    • Addendum
    • πŸ”Quiz
    • πŸ“œSummary
  • Types
    • Overview
    • Tuples
    • Records
    • Unions
    • Enums
    • Anonymous records
    • Value types
    • πŸ“œRecap
    • Addendum
  • Monadic types
    • Intro
    • Option type
    • Result type
    • Smart constructor
    • πŸš€Computation expression (CE)
    • πŸš€CE theoretical basements
    • πŸ“œRecap
  • Pattern matching
    • Patterns
    • Match expression
    • Active patterns
    • πŸš€Fold function
    • πŸ“œRecap
    • πŸ•ΉοΈExercises
  • Collections
    • Overview
    • Types
    • Common functions
    • Dedicated functions
    • πŸ”Quiz
    • πŸ“œRecap
  • Asynchronous programming
    • Asynchronous workflow
    • Interop with .NET TPL
    • πŸ“œRecap
  • Module and Namespace
    • Overview
    • Namespace
    • Module
    • πŸ”Quiz
    • πŸ“œRecap
  • Object-oriented
    • Introduction
    • Members
    • Type extensions
    • Class, Struct
    • Interface
    • Object expression
    • Recommendations
Powered by GitBook
On this page
  • Type List
  • Type alias
  • Immutability
  • Literals
  • Initialisation
  • Exercices πŸ•ΉοΈ
  • Type Array
  • Alias ResizeArray
  • Type Seq
  • Syntax
  • Infinite sequence
  • Type Set
  • Informations
  • Operations
  • Type Map
  • Access/Lookup by key
  • Dictionaries
  • dict function
  • readOnlyDict function
  • Recommendation
  • KeyValue active pattern
  • Lookup performance
  • Dictionary vs Map
  • Dictionary vs Array
  • Map and Set vs IComparable

Was this helpful?

Edit on GitHub
  1. Collections

Types

Type List

Implemented as a linked list:

  • 1 list = 1 element (Head) + 1 sub-list (Tail)

  • Construction using :: Cons operator

To avoid infinite recursion, we need an "exit" case: β†’ Empty list named Empty and noted []

πŸ‘‰ Generic and recursive union type:

type List<'T> =
  | ( [] )
  | ( :: ) of head: 'T * tail: List<'T>

☝️ Note: this syntax with cases as operator is only allowed in FSharp.Core.

Type alias

  • List (big L) refers to the Fβ™― type or its companion module.

  • list (small l) is an alias of Fβ™―'s List type, often used with OCaml notation: β†’ let l : string list = ...

⚠️ Warnings:

  • ❗ After open System.Collections.Generic:

    • List is the Cβ™― mutable list, hiding the Fβ™― type!

    • The List Fβ™― companion module remains available β†’ confusion!

  • πŸ’‘ Use the ResizeArray alias to reference the Cβ™― list directly

    • no need for open System.Collections.Generic πŸ‘

Immutability

A List is immutable: β†’ It is not possible to modify an existing list.

Adding an element in the list: = Cheap operation with the Cons operator (::) β†’ Creates a new list with: β€’ Head = given element β€’ Tail = existing list

🏷️ Related concepts:

  • linked list

  • recursive type

Literals

#
Notation
Equivalent
Meaning (*)

0

[]

[]

Empty

1

[1]

1 :: []

Cons (1, Empty)

2

[2; 1]

2 :: 1 :: []

Cons (2, Cons (1, Empty))

3

[3; 2; 1]

3 :: 2 :: 1 :: []

Cons (3, Cons (2, Cons (1, Empty)))

//...
v1@2 = FSharpList<int>.Cons(1, FSharpList<int>.Empty);
v2@3 = FSharpList<int>.Cons(2, FSharpList<int>.Cons(1, FSharpList<int>.Empty));
//...

Initialisation

// Range: Start..End (Step=1)
let numFromOneToFive = [1..5]     // [1; 2; 3; 4; 5]

// Range: Start..Step..End
let oddFromOneToNine = [1..2..9]  // [1; 3; 5; 7; 9]

// Comprehension
let pairsWithDistinctItems =
    [ for i in [1..3] do
      for j in [1..3] do
        if i <> j then
            i, j ]
// [(1; 2); (1; 3); (2, 1); (2, 3); (3, 1); (3, 2)]

Exercices πŸ•ΉοΈ

1. Implement the rev function β†’ Inverts a list: rev [1; 2; 3] ≑ [3; 2; 1]

2. Implement the map function β†’ Transforms each element: [1; 2; 3] |> map ((+) 1) ≑ [2; 3; 4]

πŸ’‘ Hints β†’ Use empty list [] or Cons head :: tail patterns β†’ Write a recursive function

Solution
let rev list =
    let rec loop acc rest =
        match rest with
        | [] -> acc
        | x :: xs -> loop (x :: acc) xs
    loop [] list

let map f list =
    let rec loop acc rest =
        match rest with
        | [] -> acc
        | x :: xs -> loop (f x :: acc) xs
    list |> loop [] |> rev

We can verify that it's working in FSI console:

let (=!) actual expected =
    if actual = expected
    then printfn $"βœ… {actual}"
    else printfn $"❌ {actual} != {expected}"

[1..3] |> rev =! [3; 2; 1];;
// βœ… [3; 2; 1]

[1..3] |> map ((+) 1) =! [2; 3; 4];;
// βœ… [2; 3; 4]

Type Array

Signature: 'T array (recommended) or 'T[] or 'T []

Main differences compared to the List:

  • Fixed-size

  • Fat square brackets [| |] for literals

  • Mutable ❗

  • Access by index in O(1) πŸ‘

// Literal
[| 1; 2; 3; 4; 5 |]  // val it : int [] = [|1; 2; 3; 4; 5|]

// Range
[| 1 .. 5 |] = [| 1; 2; 3; 4; 5 |]  // true
[| 1 .. 3 .. 10 |] = [| 1; 4; 7; 10 |] // true

// Comprehension
[| for i in 1 .. 5 -> i, i * 2 |]
// [|(1, 2); (2, 4); (3, 6); (4, 8); (5, 10)|]

// Slicing: sub-array between the given `(start)..(end)` indices
let names =    [|"0: Alice"; "1: Jim"; "2: Rachel"; "3: Sophia"; "4: Tony"|]

names[1..3] // [|            "1: Jim"; "2: Rachel"; "3: Sophia"           |]
names[2..]  // [|                      "2: Rachel"; "3: Sophia"; "4: Tony"|]
names[..3]  // [|"0: Alice"; "1: Jim"; "2: Rachel"; "3: Sophia"           |]

// Mutation
let names = [| "Juliet"; "Tony" |]
names[1] <- "Bob"
names;;  // [| "Juliet"; "Bob" |]

πŸ’‘ Slicing works also with string: "012345"[1..3] ≑ "123"

Alias ResizeArray

Alias for System.Collections.Generic.List<T> BCL type

let rev items = items |> Seq.rev |> ResizeArray
let initial = ResizeArray [ 1..5 ]
let reversed = rev initial // ResizeArray [ 5..-1..0 ]

Advantages πŸ‘

  • No need for open System.Collections.Generic

  • No name conflicts on List

Notes ☝️

  • Do not confuse the alias ResizeArray with the Array Fβ™― type.

  • ResizeArray is in Fβ™― a better name for the BCL generic List<T>

    • Closer semantically and in usage to an array than a list

Type Seq

Definition: Series of elements of the same type

't seq ≑ Seq<'T> ≑ IEnumerable<'T>

Lazy: sequence built gradually as it is iterated β‰  All other collections built entirely from their declaration

Syntax

seq { items | range | comprehension }

seq { yield 1; yield 2 }   // 'yield' explicit πŸ˜•
seq { 1; 2; 3; 5; 8; 13 }  // 'yield' omitted πŸ‘

// Range
seq { 1 .. 10 }       // seq [1; 2; 3; 4; ...]
seq { 1 .. 2 .. 10 }  // seq [1; 3; 5; 7; ...]

// Comprehension
seq { for i in 1 .. 5 do i, i * 2 }
// seq [(1, 2); (2, 4); (3, 6); (4, 8); ...]

Infinite sequence

2 options to write an infinite sequence

  • Use Seq.initInfinite function

  • Write a recursive function to generate the sequence

Option 1

Seq.initInfinite function

  • Signature: (initializer: (index: int) -> 'T) -> seq<'T>

  • Parameter: initializer is used to create the specified index element (>= 0)

let seqOfSquares = Seq.initInfinite (fun i -> i * i)

seqOfSquares |> Seq.take 5 |> List.ofSeq;;
// val it: int list = [0; 1; 4; 9; 16]

Option 2

Recursive function to generate the sequence

[<TailCall>]
let rec private squaresStartingAt n =
    seq {
        yield n * n
        yield! squaresStartingAt (n + 1) // πŸ”„οΈ
    }

let squares = squaresStartingAt 0

squares |> Seq.take 10 |> List.ofSeq;;
// val it: int list = [0; 1; 4; 9; 16; 25; 36; 49; 64; 81]

Type Set

  • Self-ordering collection of unique elements (without duplicates)

  • Implemented as a binary tree

// Construct
set [ 2; 9; 4; 2 ]          // set [2; 4; 9]  // ☝ Only one '2' in the set
Set.ofArray [| 1; 3 |]      // set [1; 3]
Set.ofList [ 1; 3 ]         // set [1; 3]
seq { 1; 3 } |> Set.ofSeq   // set [1; 3]

// Add/remove element
Set.empty         // set []
|> Set.add 2      // set [2]
|> Set.remove 9   // set [2]    // ☝ No exception
|> Set.add 9      // set [2; 9]
|> Set.remove 9   // set [2]

Informations

β†’ count, minElement, maxElement

let oneToFive = set [1..5]          // set [1; 2; 3; 4; 5]

// Number of elements: `Count` property or `Set.count` function - ⚠️ O(N)
// ☝ Do not confuse with `Xxx.length` for Array, List, Seq
let nb = Set.count oneToFive // 5

// Element min, max
let min = oneToFive |> Set.minElement   // 1
let max = oneToFive |> Set.maxElement   // 5

Operations

Operation
Operator
Function for 2 sets
Function for N sets

βŠ–

Difference

-

Set.difference

Set.differenceMany

βˆͺ

Union

+

Set.union

Set.unionMany

∩

Intersection

Γ—

Set.intersect

Set.intersectMany

Examples:

| Union             | Difference        | Intersection      |
|-------------------|-------------------|-------------------|
| Β  [ 1 2 3 4 5 Β  ] | Β  [ 1 2 3 4 5 Β  ] | Β  [ 1 2 3 4 5 Β  ] |
| + [   2   4   6 ] | - [   2   4   6 ] | ∩ [   2   4   6 ] |
| = [ 1 2 3 4 5 6 ] | = [ 1 Β  3 Β  5 Β  ] | = [ Β  2 Β  4 Β  Β  ] |

Type Map

Associative array { Key β†’ Value } ≃ Cβ™― immutable dictionary

// Construct: from collection of (key, val) pairs
// β†’ `Map.ofXxx` function β€’ Xxx = Array, List, Seq
let map1 = seq { (2, "A"); (1, "B") } |> Map.ofSeq
// β†’ `Map(tuples)` constructor
let map2 = Map [ (2, "A"); (1, "B"); (3, "C"); (3, "D") ]
// map [(1, "B"); (2, "A"); (3, "D")]
// πŸ‘‰ Ordered by key (1, 2, 3) and deduplicated in last win - see '(3, "D")'

// Add/remove entry
Map.empty         // map []
|> Map.add 2 "A"  // map [(2, "A")]
|> Map.remove 5   // map [(2, "A")] // ☝ No exception if key not found
|> Map.add 9 "B"  // map [(2, "A"); (9, "B")]
|> Map.remove 2   // map [(9, "B")]

Access/Lookup by key

let table = Map [ ("A", "Abc"); ("G", "Ghi"); ("Z", "Zzz") ]

// Indexer by key
table["A"];;  // val it: string = "Abc"
table["-"];;  // πŸ’£ KeyNotFoundException

// `Map.find`: return the matching value or πŸ’£ if the key is not found
table |> Map.find "G";; // val it: string = "Ghi"

// `Map.tryFind`: return the matching value in an option
table |> Map.tryFind "Z";;  // val it: string option = Some "Zzz"
table |> Map.tryFind "-";;  // val it: string option = None

Dictionaries

dict function

  • Builds an IDictionary<'k, 'v> from a sequence of key/value pairs

  • The interface is error prone: the dictionary is immutable ❗

let table = dict [ (1, 100); (2, 200) ]
// val table: System.Collections.Generic.IDictionary<int,int>

table[1];;          // val it: int = 100

table[99];;         // πŸ’£ KeyNotFoundException

table[1] <- 111;;   // πŸ’£ NotSupportedException: This value cannot be mutated
table.Add(3, 300);; // πŸ’£ NotSupportedException: This value cannot be mutated

readOnlyDict function

  • Builds an IReadOnlyDictionary<'k, 'v> from a sequence of key/value pairs

  • The interface is honest: the dictionary is immutable

let table = readOnlyDict [ (1, 100); (2, 200) ]
// val table: System.Collections.Generic.IReadOnlyDictionary<int,int>

table[1];;          // val it: int = 100

table[99];;         // πŸ’£ KeyNotFoundException

do table[1] <- 111;;
// ~~~~~~~~ πŸ’₯ Error FS0810: Property 'Item' cannot be set

do table.Add(3, 300);;
//       ~~~ πŸ’₯ Error FS0039:
// The type 'IReadOnlyDictionary<_,_>' does not define the field, constructor or member 'Add'.

Recommendation

dict returns an object that does not implement fully IDictionary<'k, 'v> β†’ Violate the Liskov's substitution principle❗

readOnlyDict returns an object that respects IReadOnlyDictionary<'k, 'v>

πŸ‘‰ Prefer readOnlyDict to dict when possible

KeyValue active pattern

Used to deconstruct a KeyValuePair dictionary entry to a (key, value) pair

// FSharp.Core / prim-types.fs#4983
let (|KeyValue|) (kvp: KeyValuePair<'k, 'v>) : 'k * 'v =
    kvp.Key, kvp.Value

let table =
    readOnlyDict
        [ (1, 100)
          (2, 200)
          (3, 300) ]

// Iterate through the dictionary
for kv in table do // kv: KeyValuePair<int,int>
    printfn $"{kv.Key}, {kv.Value}"

// Same with the active pattern
for KeyValue (key, value) in table do
    printfn $"{key}, {value}"

Lookup performance

Dictionary vs Map

readOnlyDict creates high-performance dictionaries β†’ 10x faster than Map for lookups

Dictionary vs Array

~ Rough heuristics

β†’ The Array type is OK for few lookups (< 100) and few elements (< 100) β†’ Use a Dictionary otherwise

Map and Set vs IComparable

Only works if elements (of a Set) or keys (of a Map) are comparable!

Examples:

// Classes are not comparable by default, so you cannot use them in a set or a map
type NameClass(name: string) =
    member val Value = name

let namesClass = set [NameClass("Alice"); NameClass("Bob")]
//                    ~~~~~~~~~~~~~~~~~~
// πŸ’₯ Error FS0193: The type 'NameClass' does not support the 'comparison' constraint.
//     For example, it does not support the 'System.IComparable' interface

F# functional type: tuple, record, union

// Example: single-case union
type Name = Name of string

let names = set [Name "Alice"; Name "Bob"]

Structs:

[<Struct>]
type NameStruct(name: string) =
    member this.Name = name

let namesStruct = set [NameStruct("Alice"); NameStruct("Bob")]

Classes implementing IComparable... but not IComparable<'T> 🀷

PreviousOverviewNextCommon functions

Last updated 1 month ago

Was this helpful?

(*) We can verify it with :

πŸ’‘ Bonus: verify the tail recursion with

πŸ”— β€’ Compositional IT (2021)

SharpLab.io
sharplab.io
High Performance Collections in Fβ™―