A generic type or function has type parameters, written 'a, 'T or any name after a single quote. Most F# code is generic without saying so: the compiler generalises a function when its body does not fix a type (automatic generalisation), so let swap (a, b) = (b, a) has the type 'a * 'b -> 'b * 'a.
Writing type parameters
let pair<'a, 'b> (x: 'a) (y: 'b) : 'a * 'b = x, y // explicit parameters
let first (x: 'a, _: 'b) = x // implicit ones
type Box<'T>(value: 'T) =
member _.Value = value
type Cache<'K, 'V when 'K : equality>() =
let d = System.Collections.Generic.Dictionary<'K, 'V>()
member _.Add(k, v) = d.[k] <- v
let b = Box<int>(3) // explicit instantiation
let c = Box 3 // inferred
let xs : List<_> = [ 1; 2 ] // _ is a type to infer
let t = typeof<int list> // a type argument in an expression
let e = Seq.empty<string>
<), in annotations and as arguments.The angle brackets must touch the name (List<int>, not List <int>) so that the lexer treats them as a type application and not as less-than. In a type definition the parameters follow the name; on a function they follow the name before its parameters. An unused parameter written _ lets the compiler infer it. The .NET names List<T> and IEnumerable<T> and the F# postfix names (int list, int seq) are the same types.
Constraints
A constraint limits the types a parameter accepts. It is written after when inside the parameter list, or after the parameters of a function; several are joined with and.
| Constraint | Syntax | The type must |
|---|---|---|
| type | 'T :> Type | be, derive from, or implement the type |
| null | 'T : null | admit null (a .NET reference type, not an F# list, tuple, function, class, record or union) |
| not null | 'T : not null | not admit null (F# 9 nullness checking) |
| member | 'T : (member Name: sig), (^A or ^B) : (static member (+) : ^A * ^B -> ^C) | have a member of that signature (see below) |
| constructor | 'T : (new : unit -> 'T) | have a public parameterless constructor |
| value type | 'T : struct | be a .NET value type |
| reference type | 'T : not struct | be a .NET reference type |
| enumeration | 'T : enum<int> | be an enum over that type |
| delegate | 'T : delegate<args, result> | be a delegate of that signature |
| comparison | 'T : comparison | support compare and the ordering operators |
| equality | 'T : equality | support = and hashing |
| unmanaged | 'T : unmanaged | be an unmanaged type (certain primitives, enums, pointers and structs of those) |
let sortDistinct (xs: 'T list) : 'T list when 'T : comparison = xs |> List.distinct |> List.sort
let create<'T when 'T : (new : unit -> 'T)> () = new 'T()
let dispose (x: 'T when 'T :> System.IDisposable) = x.Dispose()
let both (x: 'T when 'T :> System.IDisposable and 'T :> seq<int>) = ()
let both2 (x: #System.IDisposable & #seq<int>) = () // F# 8: intersection of flexible types
:> clauses.Flexible types
#Type in an annotation means “this type or any type that derives from or implements it”: (x: #seq<int>) accepts a list, an array or any other sequence of int. It is a short way to write a constrained parameter 'a when 'a :> seq<int>. F# inserts flexibility itself at method calls, so a method taking an obj accepts a string without a cast, while a function argument that is an F# function value needs an explicit # or :>.
Statically resolved type parameters and inline
A statically resolved type parameter is replaced by a real type at compile time. Only an inline function can have one, and its main use is a member constraint: the type must have a member with a given name and signature, as numeric operators need. The library’s own operators are written this way, so (+) works on every type that has a +.
let inline add (x: ^T) (y: ^U) : ^V when (^T or ^U) : (static member (+) : ^T * ^U -> ^V) = x + y
let inline length (x: ^T when ^T : (member Length: int)) = x.Length
let inline zero<'T when 'T : (static member Zero : 'T)> = 'T.Zero // F# 7: a member of the type parameter itself
let a = add 1 2
let b = add 1.5 2.5
let n = length "text"
^T; from F# 7 'T works too.Each use of an inline function gets its own copy of the code with the type filled in, which is why such functions are fast and cannot be used as first-class values with a generic type. inline also inlines a small function into its caller; [<InlineIfLambda>] on a parameter inlines a lambda passed to it (F# 6). [<NoCompilerInlining>] (F# 7) and [<NoDynamicInvocation>] restrict this. From F# 11 the solution of a constraint may use an extension member (ExtensionConstraintSolutions).
Not described
Known, not yet covered
- Reference: the exact rules for constraint solving and for default and weak resolution of type variables (the inference chapter of the specification was not read); the meaning of each constraint on
comparisonandequalityfor composite types; the'T.Membersyntax details (F# 7, “simplify constrained call syntax”); extension member constraints of F# 11; generic type parameters on modules, which F# does not have. - Reference: the pages on automatic generalisation, inline functions and extension member constraints in the documentation were read for headings and the constraint table only; the examples were not compiled and the SRTP examples were written for this page.
See also
- Types – the type syntax
- Functions and closures –
inlineand generalisation of functions