When Type Constraints Meet Reflection In F#

When you use a “type constraint” in F#, the kind of constraint that specifies that the type has to extend from some other type, it’s possible to confuse the compiler (or more accurately: confuse yourself) into allowing some code that is guaranteed to fail at runtime.

Here’s how it can happen. If you have a constraint like <'a when 'a :> IFace>, where IFace is an interface type, if the compiler doesn’t have enough information to establish a concrete type for 'a, then at runtime, typeof<'a> will actually be IFace, not some derived, concrete type as you might have hoped. This may sound obvious, but it probably won’t be, the first time it happens to you.

Below is a code sample that gives a real-world example. Here we’re trying to instantiate a record type via reflection. In two of the test cases, we give the compiler enough information, and it correctly uses a concrete type. When we don’t provide enough, it defaults back to IFace, which leads to trouble – it tries to instantiate IFace as if it’s a record type, which certainly doesn’t work. This isn’t a bug – after all, in pretty much any situation not involving reflection, the supertype is a reasonable default – but it can be surprising.

It would be great to have a new kind of type constraint that could insist that the type be instantiable, i.e. concrete, non-abstract. Maybe something like <'a when 'a :> IFace and 'a:concrete>. With such a constraint in place, we’d get a compile error instead of a runtime error if the compiler couldn’t prove that the type was concrete at any particular call site.

For the moment, though, just be aware that there is no such constraint, and you have to consider the possibility that your constrained type might just be an interface type. There is the 'a : (new: unit->'a) constraint that works just like the new constraint in C#, and that would help, except it only works with a no-args constructor, which wouldn’t be appropriate for our example at all, since we don’t even know ahead of time how many arguments we’re going to pass the constructor.

This is just something to be aware of, so when you get exceptions with messages like “Type ‘ConstraintTest+IFace’ is not an F# record type,” or “Constructor on type ‘ConstraintTest+IFace’ not found,” you don’t spend too much time thinking “Of course it isn’t! Why did you think that would work, compiler?”

By the way, Matt Podwysocki has a great series on what you can do with constraints in F#: Part 1, Part 2, Part 3. You should definitely head there next.

module ConstraintTest
open NUnit.Framework

    //A simple interface
    type IFace =
        abstract member X: int

    //A record type that implements the interface
    type Foo = {FooX:int}
        with
            interface IFace with
                member this.X = this.FooX

    //A function that instantiates a new record
    //There's a type constraint to ensure the record type implements IFace.
    let makeRecord<'a when 'a :> IFace> (values: obj array) =
        let record = Activator.CreateInstance(typeof<'a>, values)
        record :?> 'a

    [<Test>]
    let ``insufficient type info``() =
        //F# can't figure out the concrete type, defaults to substituting IFace for 'a
        let exc = Assert.Throws<System.MissingMethodException>(fun () -> makeRecord [|1|] |> ignore)
        printf "%A\n" exc

    [<Test>]
    let ``specified type``() =
        //We help F# figure out what the concrete type should be, no problem.
        let record:Foo = makeRecord [|1|]
        ()

    [<Test>]
    let ``specified type 2``() =
        //Again, we help F# figure out what the concrete type should be (in a different way), no problem.
        let record = makeRecord<foo> [|1|]
        ()

One Comment

  • Ganesh Sittampalam wrote:

    I think it would be more accurate to say that using reflection makes it very easy to write code that is guaranteed to fail at runtime. Static constraints can’t get anywhere close to expressing all the things you can do with reflection.