Generically Constraining F# – Part I
Generic constraints inside .NET has always been a fun enterprise, especially given how C# handles them There has been some discussion on Jon Skeet’s blog about the fact that C# does not allow for generic constraints referring to a number of types. These include:
- System.Array
- System.Delegate
- System.Enum
- System.Object
- System.ValueType
This is indeed a bit unfortunate, as it limits some of the more interesting applications. The example Jon shows is indeed illegal in C#:
public static T[]GetValues<T>() where T : struct, System.Enum { return (T[]) Enum.GetValues(typeof(T)); }
However, as Jon correctly points out, this is indeed supported by the CLR directly. In fact, with our knowledge of F# constraints, we can write this exact function in F# without any such issue. It’s little wonder that F# has learned some of their lessons from C#, but as well as having the language designed by the person who brought generics to .NET also helps.
Let’s first look at our F# implementation. The idea here is to ensure that our T type as above is an enum. In order to do so, we must specify it is of type enum<underlying-type> where the underlying type is most usually an Int32. Remember, enums can be of any integral besides char, so that’s why it must be specified.
namespace Codebetter.Constraints module Constraint = open System let getValues<'a, 'b when 'a : enum<'b>>() = Enum.GetValues(typeof<'a>) :?> 'a array
As we can see from the above code, it’s rather straight forward. We specify the ‘a must be an enum of an inner type of ‘b. We could have simplified this for an external call to export it using an int as our ‘b, but let’s keep this as generic as possible. Calling this code, we can get arrays of all values. Let’s test in F# interactive:
> Constraint.getValues<System.IO.FileAccess,int>();; val it : System.IO.FileAccess array = [|Read; Write; ReadWrite|] > Constraint.getValues<string,int>();; Constraint.getValues<string,int>();; ^^^^^^^^^^^^^^^^^^^^ error FS0001: The type 'string' is not a .NET enum type
In our first example, we call with System.IO.FileAccess in which it gives us the three enum values, and in our second case, we tried with a non-enum type of string and sure enough it tells us as much. But, what about C# here? Could this code transfer? In some C# calling code, we could then use our function as follows:
static void Main(string[] args) { var values = Constraint.getValues<FileAttributes, int>(); foreach(var value in values) Console.WriteLine(value); }
This will give us the expected results of all values for the FileAttributes enum just as we would through our F# code. An issue arises, however, when we try with a failure case as we had above:
var values = Constraint.getValues<string, int>(); foreach (var value in values) Console.WriteLine(value);
This will compile just as our previous example did. When we run this example, however, we get an exception thrown due to Enum.GetValues expecting an enum type as we have below and our constraint not honored.
Unhandled Exception: System.ArgumentException: Type provided must be an Enum. Parameter name: enumType at System.Enum.GetValues(Type enumType) at Codebetter.Constraints.Constraint.getValues[a,b]() in C:\Work\ConstraintLib\Module1.fs:line 19
So, the important thing to remember is that even if other languages export generic restrictions such as an enum, C# will not honor this restriction. Other restrictions such as reference type restrictions such as the following work well:
let printClass<'a when 'a : not struct> (arg:'a) = printfn "%A" arg
And the calling C# code:
Constraint.printClass("Hello") // prints Hello Constraint.printClass(3) // error CS0452
By the code above, we sure enough have the compiler honoring our generic restrictions in this case.
Conclusion
We’re just getting started here in this brief series to cover generic constraints. There are a few more to talk about before we’re done such as method signature restrictions, constructor restrictions and so forth. I hope at some interation that generic restrictions in C# get revisited to make them as fully features as F#’s.