Last time we looked at some of the ways in which generic type constraints can be constrained in F#. This time we'll cover the remaining type constraint mechanisms.
Table of Contents
- Equality and comparison constraints
- Delegate constraints
- Unmanaged constraints
- Null constraints
- Combining multiple constraints
Using the equality constraint will restrict the type to any type that is not decorated with the NoEquality custom attribute. Even types that do not define their own equality implementation will satisfy this constraint.
[<NoEquality; NoComparison>] type Foo(x : int) = class end type ClassDef<'T when 'T : equality>() = class end let FuncDef( a : 'a when 'a : equality) = () let usage = // Any type is Ok here, even if it doesnt support equality new ClassDef<string>() |> ignore new ClassDef<System.IO.StreamReader>() |> ignore FuncDef("") // This however will not compile new ClassDef<Foo>() |> ignore ()
The comparison constraint is very similar except that the type must implement IComparable, be an array with comparable elements, be System.IntPtr, be System.UIntPtr and must not be decorated with the NoComparison custom attribute.
[<NoEquality; NoComparison>] type Foo(x : int) = class end type ClassDef<'T when 'T : comparison>() = class end let funcDef( a : 'a when 'a : comparison) = () let usage = // Any type is Ok here, even if it doesnt support equality new ClassDef<string>() |> ignore // This time we can't use StreamReader because it does not implement // IComparable //new ExampleClassX<System.IO.StreamReader>() |> ignore funcDef("") // This however will not compile new ClassDef<Foo>() |> ignore ()
We can also restrict types to being both equatable and comparable
type ClassDef<'T when 'T : comparison and 'T : equality>() = class end let funcDef( a : 'a when 'a : comparison and 'a : equality) = ()
Probably what will be the least used and most obscure of the constraints. This constraint doesn't actually do what you might want it to. When originally writing this post I assumed that a delegate constraint would allow you to restrict the type parameter to an arbitrary delegate and then be able to invoke it from the function body. It turns out this was a spurious assumption. Here's my completely incorrect assumption about how the delegate constraints could be used:
(* NOTE: this will not compile, it merely demonstrates my assumptions that turned out to be incorrect *) type Foo = delegate of (string * string) -> unit let funcDef(a : 'a when 'a : delegate<(string * string), unit>) = a("a", "b") () let usage = funcDef(Foo(fun a b -> printf "%s" (a + b)))
The compiler complains about "Foo not being a standard delegate type". If we take a look at the compiler source we can see that the first parameter of the delegate must be of type object and also that the first delegate input parameters cannot be wrapped in (). F# treats the following delegate definitions differently
type Foo = delegate (String * String) -> unit type Foo' = delegate String * String -> unit
F#'s definition of a "standard" delegate type is (from the source)
interpret a delegate type as a "standard" .NET delegate type associated with an event, with a "sender" parameter.
Armed with this knowledge we can try and redefine our types and constraints to conform to the "standard delegate" format.
type Foo = delegate of obj * String -> unit let funcDef(a : 'a when 'Del : delegate<obj * string,unit>) = a("a", "b") () let usage = funcDef(Foo(fun a b -> printf "Output %s %s" (a.ToString()) b)) ()
Unfortunately, this is still incorrect, for a number of different reasons. First, when declaring the delegate constraint we must omit the obj parameters. Even though we must supply the obj parameter when invoking the delegate we do not include it in the type constraint. Secondly, we must also constrain the parameter to be derived from the System.Delegate base type so that we can use the DynamicInvoke method. Once we make these required changes we'll end up with the following, which does successfully compile and the correct delegate is invoked:
// Our delegate MUST start have obj as the first argument. The arguments MUST be // tupled. The argument MUST NOT be wrapped in parentheses. type Foo = delegate of obj * string * string -> unit // When defining the constraint we MUST omit the obj parameter. We MUST also // constrain to the System.Delegate base type. This is because the only way to // invoke the delegate is via the DynamicInvoke method. let funcDef(a : 'a when 'Del : delegate<string * string,unit> and 'a :> System.Delegate ) = a.DynamicInvoke(([| "a"; "b"; "c"|] : obj)) |> ignore () let usage = funcDef(Foo(fun a b c -> printf "Output %s %s %s" (a.ToString()) b c)) ()
Unmanaged constraints will constrain the type to be an "unmanaged" type. More specifically, this is any primitive type (sbyte, byte, char, nativeint, unativeint, float32, float64, int16, uint16, int32, uint32, int64, uint64, decimal), any enumeration type, any non-generic structure with fields consisting only of unmanaged types, and finally, nativeptr<_>
type ClassDef<'T when 'T : unmanaged>(x : 'T) = class end let funcDef(a : 'a when 'a : unmanaged) = () let usage = new ClassDef<int>(1) |> ignore funcDef 1uy
Null constraints will constrain a generic type such that the type must be nullable. This includes all .NET reference types but does not include F# lists, types, functions, classes, records or unions.
type Foo() = class end type ClassDef<'T when 'T : null>() = class end let funcDef(a : 'a when 'a : null) = () let usage = new ClassDef<String>() |> ignore funcDef (new System.IO.StringReader("")) // This is not valid though new ClassDef<Foo>() |> ignore funcDef 
It's possible to mix and match the above constraints in different ways.
// 'a must derive from interface IFoo, support equality and have a parameterless // constructor. let example1 (a : 'a when 'a :> IFoo and 'a :> equality and 'a : (new : unit -> 'a))= ()
As I've noted in previous sections, it's possible to constrain a type parameter such that the constraint can never be fulfilled. It's easy to see if a type parameter is accidentally impossibly constrained so it's easy to fix but it's worth keeping it in mind.