Skip to content
AgileLogics
6 min read

Generics

Prerequisites: Discriminated unions

Generic code works with any type. You've used it all along — List.map works on lists of anything — and F# makes your own functions generic automatically when it can:

let swap (a, b) = (b, a)
let firstOr fallback list =
    match list with
    | [] -> fallback
    | x :: _ -> x

printfn "%A" (swap (1, "one"))
printfn "%A" (swap ("left", true))
printfn "%d" (firstOr 0 [])
printfn "%s" (firstOr "none" ["a"; "b"])
("one", 1)
(true, "left")
0
a

Their inferred types are 'a * 'b -> 'b * 'a and 'a -> 'a list -> 'a. Automatic generalization means you rarely write type parameters yourself.

Generic types

Records and unions can take type parameters, written in angle brackets:

type Pair<'T> = { First: 'T; Second: 'T }

type Tree<'T> =
    | Leaf
    | Node of Tree<'T> * 'T * Tree<'T>

let rec size tree =
    match tree with
    | Leaf -> 0
    | Node (l, _, r) -> size l + 1 + size r

let p = { First = 1; Second = 2 }
let names = Node (Leaf, "b", Node (Leaf, "c", Leaf))

printfn "%A" p
printfn "%d" (size names)
{ First = 1
  Second = 2 }
2

Option<'T>, Result<'T, 'E>, List<'T>, and Map<'K, 'V> are all defined this way. For the built-in types, F# also allows the "postfix" style: int option is Option<int>, and string list is List<string>.

Constraints

Sometimes a generic type must support something — comparison, equality, or an interface. F# infers these constraints too, and shows them with when:

let largest list = List.max list          // 'a list -> 'a when 'a: comparison
let contains x list = List.exists ((=) x) list   // when 'a: equality

printfn "%d" (largest [3; 9; 2])
printfn "%s" (largest ["pear"; "apple"])
printfn "%b" (contains 3 [1; 2; 3])

let describeAll<'T when 'T :> System.IFormattable> (items: 'T list) =
    items |> List.map (fun i -> i.ToString("N2", System.Globalization.CultureInfo.InvariantCulture))

printfn "%A" (describeAll [1.5; 1000.0])
9
pear
true
["1.50"; "1,000.00"]

'T :> IFormattable means "'T must implement IFormattable". Most of the time inference handles constraints for you; you write them explicitly on public APIs and when calling interface methods.

inline and statically resolved types

A normal generic function can't use + on "any type" — + needs to know the type. Mark the function inline and F# resolves the operator at each call site instead:

let inline double x = x + x

printfn "%d" (double 21)
printfn "%.1f" (double 1.5)
printfn "%s" (double "ab")
printfn "%M" (double 2.5m)
42
3.0
abab
5.0

That's how built-ins like List.sum work on ints, floats, and decimals alike. Level 3 goes deeper into inline.

Common mistakes

  • Value restriction: let empty = [] at the top level can't be generalized in some contexts and gives error FS0030. Give it a type (let empty: int list = []) or make it a function.
  • Forgetting that 'T means "the caller chooses": a function returning 'T can't just return an int.