Generic type constraints by example - Part 1

26 Sep 2014

Introduction

F#, like C# before it, provides a number of different ways to constrain a generic type argument. In a previous post I touched on using statically resolved type parameters along with member constraints in order to fake Haskell's typeclasses. One of the main problems I had with experimenting with member constraints was the sligtly confusing and unfamiliar syntax. This post is a set of examples of the syntax and usage of statically resolved type parameters and the constraints you can place upon them. For completeness I also briefly give examples of the other ways in which generic types can be constrained in F#, ordered by what I think is most-to-least interesting. I've covered half in this post and I'll cover the other half in a subsquent post.

Table of Contents

  1. Member constraints
  2. Constructor contraints
  3. Base type constraints
  4. Enum constraints
  5. Interface type constraints
  6. Reference and value type constraints

Member constraints

Member contraints constrain the type not just to a base class or interface but instead to the specific members that are required by the function. As long as a type can provide implementations for the required members then the constraint is fulfilled, regardless of its type or type heirarchy. This is commonly known as duck typing. Member constraints can only be used with statically resolved type parameters and inline functions. For the rest of this section we'll use the following datatype for all the examples:

type Foo() =  
    member this.NoArgProperty : int = 1
    member this.NoArgMethod() : int = 1
    member this.SelfReferentialMethod (a : Foo) : int = 1
    member this.MultiArgMethodTupled (a : int, b: int) : int = a + b
    member this.MultiArgMethodNonTupled (a : int) (b: int) : int = a + b
    static member StaticNoArgMethod() : int = 1
    static member StaticMultiArgMethodTupled (a : int, b: int) : int = a + b
    static member StaticMultiArgMethodNonTupled (a : int) (b: int) : int = a + b

Let's see some simple usage examples:

let inline example1 (a : ^a) = 

    // Invoke member function that takes no arguments and returns an int
    let result1 = (^a : (member NoArgMethod : unit -> int) a)

    // Invoke member property that returns an int
    let result2 = (^a : (member NoArgProperty : int ) a)

    // Invoked static function that takes no arguments and returns an int
    let result3 = (^a : (static member StaticNoArgMethod : unit -> int) ())
    ()

let usage = 
    example1 (Foo())

What the above example does is invoke various instance/static methods/properties on the instance a. According to the F# documentation we can annotate the example1 function to declare the explicit members that are required.

let inline example2
     (a : ^a when 
        ^a : (member NoArgMethod : unit -> int) and
        ^a : (member NoArgProperty : int) and 
        ^a : (static member StaticNoArgMethod : unit -> int))  = 

    let result = (^a : (member NoArgMethod : unit -> int) a)

    // Although we've explicitly declared the required members we cannot 
    // do this:
    //let result1 = a.NoArgMethod           //Will not compile!
    //let result2 = a.NoArgProperty         //Will not compile!
    //let result3 = (^a).StaticNoArgMethod  //Will not compile!
    ()

let usage = 
    example2 (Foo())

It seems strange that even though we've explicitly defined the required member functions in the method signature that we cannot just do a.NoArgMethod() in the method body, we have to use the more verbose invocation syntax.

F# allows member functions to be declared using either curried or tupled form. It seems that the usage of member constraints is restricted to member functions that are declared in tupled form only.

let inline example3 (a : ^a) = 
    let result0 = (^a : (static member StaticMultiArgMethodTupled : int * int -> int) 1,2)
    let result1 = (^a : (member MultiArgMethodTupled : int * int -> int) a,1,2)

    // Strangely, even though MultiArgTupled is a tupled form function we can still
    // use the following type annotation to invoke the method. It would seem
    // inadvisable to do so though to avoid confusion.
    let result2 = (^a : (member MultiArgMethodTupled : int -> int -> int) a,1,2)

    // It seems that trying to use a non tupled form method will not compile. I could
    // not find a legal syntax that would let me invoke the MultiArgMethodNonTupled
    // function.
    //let result3 = (^a : (member MultiArgMethodNonTupled : int -> int -> int)(a 1 2))
    ()

let usage = 
    example3 (Foo())

Finally, member constraints are allowed to be self referential. i.e. they can refer to their own type in the constraint definition:

let inline example4 (a : ^a) = 
    // Note that we're to mentioning 'a' twice. Once to declare which instance to 
    // invoke the method on, next to pass the instance into the method.
    let result = (^a : (member SelfReferentialMethod : ^a -> int) a,a)

    // This is perhaps clearer
    let result2 = (^a : (member SelfReferentialMethod : ^a -> int) a, (new Foo()))
    ()

let usage = 
    example4 (Foo())

You may have noticed that each of the above example functions was declared using the inline keyword. The inline keyword causes the F# compiler to emit duplicate versions of the function, one for each concrete type that the function is invoked with. The F# language specification is surprisingly light on details related to the inline, perhaps I'll make this a topic for a future post. In any case, if in doubt experiment and benchmark the results.

Constructor constraints

Constructor constraints constrain the type parameter to having a parameterless constructor.

type Foo() = class end

type ClassDef<'T when 'T : (new : unit -> 'T)>() = class end

let usage = 
    new ClassDef<Foo>() |> ignore
    ()

Looking at the syntax I thought that it would be possible to constrain type arguments to having any arbitrary constructor. Unfortuantely, the following is invalid and will not compile.

type Foo(x : int) = class end

// This fails with "'new' constraints must take one argument of type 'unit' and
// return the constructed type"
type ClassDef<'T when 'T : (new : int -> 'T)>() = class end

let usage = 
    new ClassDef<Foo>() |> ignore
    ()

I started looking at the source for the F# compiler, specifically the code that was responsible for type constraints. I thought perhaps it's something I could experiment with and add to the compiler. After toying around with the code for a while it suddenly dawned on me why F# doesn't allow this. It's not a limitation of the F# language or the F# compiler, it's a limitation of the Intermediary Language that the F# code compiles to. Even if it was possible for F# to allow for arbitrary constructor constraints there would simply be no way to represent this in the compiled assembly. If we take a look at the ECMA standard for IL (section II.10.1.7) we can see the full set of constraints that can be expressed in IL. We're limited to only parameterless constructors.

Base type constraints

Base type constraints force the type parameter to be equal to or derived from the type specified.

type Foo(x : int) = class end

type Bar(y : int)= inherit Foo(y)

// Contraints in a type defintion
type ClassDef<'T when 'T :> Foo >(x : 'T) = class end

//Constraints in a function definition
let funcDef (a : 'a when 'a :> Foo) = ()

let usage = 
    let c1 = ClassDef(Foo(1))
    let c2 = ClassDef(Bar(1))
    let c3 = funcDef(Foo(1))
    let c4 = funcDef(Bar(1))
    ()

Type with more than one generic type argument can be constrained like this:

type ClassDef<'T, 'U when 'T :> Foo and 'U :> System.IO.Stream >() = class end
let funcDef<'T, 'U when 'T :> Foo and 'U :> System.IO.Stream > = ()

Enumeration constraints

An often-requested feature of C# is to allow a type to be constrained to an enumeration. F# does allow this but it's a little more complicated than you might think. The CLR forces all enumerations to derive from a a simple base type, the default being Int32. When declaring the type constraint in F# you have to state the base type for the enumeration.

type Int32Enum = 
    | Value1 = 1
    | Value2 = 2

type ByteEnum = 
    | Value1 = 1uy
    | Value2 = 2uy

type ClassDef<'T when 'T : enum<int32> >() = class end
let funcDef (a : 'a when 'a : enum<int32>) = ()

let usage = 
    new ClassDef<Int32Enum>() |> ignore
    funcDef(Int32Enum.Value1)

    //The following is not allowed
    new ClassDef<Byte32Enum>() |> ignore
    funcDef(ByteEnum.Value1)

Interface type constraints

Interface type constraints restrict the type parameter to types that either directly or indirectly implement the required set of interfaces.

type IFoo = 
    abstract member GetFirst : unit -> int

type IBar = 
    abstract member GetSecond : unit -> string

type ImplIFooIBar = 
    interface IFoo with
        member this.GetFirst() = 1
    interface IBar with
        member this.GetSecond() = "2"

type ClassDef<'T when 'T :> IFoo >() = 
    let f (x : 'T) = x.GetFirst()

let usage = 
    // ImplIFooIBar satisfies the constraint
    new ClassDef<ImplIFooIBar>() |> ignore

    // String does not
    //new ClassDef<string>() //will not compile
    ()

With interface constraints you can sensibly restrict a type parameter to implementing more than one interface, unlike base class constraints.

type ClassDef<'T when 'T :> IFoo and 'T :> IBar >() =     
    let f (x : 'T) = 
        x.GetFirst() |> ignore
        x.GetSecond() |> ignore
        ()

let funcDef (a : 'a when 'a :> IFoo and 'a :> IBar) = 
    a.GetFirst() |> ignore
    a.GetSecond() |> ignore
    ()

let usage = 
    // ImplIfooIBar implements both required interfaces and will
    // satisfy the constraint.
    new ClassDef<ImplIFooIBar>() |> ignore
    funcDef (ImplIFooIBar())
    ()

Reference and value type constraints

Reference and value type constraints constrain a type to be either a reference type or a value type. They work in the exact same manner as they work in C#.

type ClassDefVal<'T when 'T : struct>() = class end

type ClassDefRef<'T when 'T : not struct>() = class end

let funcDefVal( a : 'a when 'a : struct) = ()

let funcDefRef( a : 'a when 'a : not struct) = ()

let usage = 
    new ClassDefVal<int>() |> ignore
    new ClassDefRef<string>() |> ignore

    funcDefVal(1)
    funcDefRef("")
    ()

Interestingly, it's possible to create an unfullfillable constraint:

type ClassDef<'T when 'T : struct and 'T :> System.IO.Stream>() = class end

let usage = 
    // There is no type we can use here and have it be correct
    new ClassDef<unknown>() |> ignore

The F# compiler will prevent you from doing this though:

type ClassDef<'T when 'T : struct and 'T : not struct>() = class end

Next time

We'll cover the remaining ways in which generic type parameters can be constrained in F#.

Published on 26 Sep 2014 permalink Find me on Github

comments powered by Disqus