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
'Tmeans "the caller chooses": a function returning'Tcan't just return anint.