It's well known that C# doesn't permit certain types to appear as constraints, like System.Enum or System.Delegate, and that C# further prevents sealed classes from appearing as constraints. However, the example in the above article serves to point to an interesting circumvention of C#'s limitations on constraints.
Suppose we wish to provide a statically typed enum interface, similar to what I provide in my Sasa class library, but without having to resort to IL rewriting as I do. We can implement this by exploiting the fact that type constraints are inherited. Consider the following general base class:
public abstract class Constrained<TArg0, TReturn> { public abstract T1 Apply<T0, T1>(T0 arg0) where T0 : TArg0 where T1 : TReturn; }
Notice how all the type parameters at the class level are fully abstract, so the C# compiler can't complain about using Enum or Delegate at this level. The class then constrains the type parameters at the abstract method level based on the class-level type constraints. An statically type-safe enum parser is then simply:
public class EnumParser : Constrained<string, Enum> { public override T1 Apply<T0, T1>(T0 arg0) { return (T1)Enum.Parse(typeof(T1), arg0); } } ... var uint16 = new EnumParser().Apply<TypeCode, string>("UInt16");
Despite the fact that the method type parameters T0 and T1 look fully abstract, they inherit the constraints of T0 : string and T1 : Enum from the base class, and so the C# compiler knows they have the correct types. Of course, Enum.Parse returns System.Object, so we have to cast to return the correct enum type.
Since EnumParser has no state, you can cache it in a static field:
public static class Enums { public static readonly EnumParser Parse = new EnumParser(); } ... var uint16 = Enums.Parse.Do<TypeCode, string>("UInt16");
You can also define more specialized base classes other than Constrained to eliminate the need for two type arguments on Apply:
public abstract class ParseConstrained<TReturn> { public abstract T Apply<T>(string arg0) where T : TReturn; } public class EnumParser : ParseConstrained<Enum> { public override T Apply<T>(string arg0) { return (T)Enum.Parse(typeof(T), arg0); } } ... var uint16 = Enums.Parse.Do<TypeCode>("UInt16");
I'm currently exploring whether it's possible to define a truly reusable set of base classes like Constrained. There are two general problems here:
- A general set of base classes won't have type constraints between method parameters, ie. like T0 : T1, which are sometimes needed.
- Covariant type safety really requires that the constraint on the return type be TReturn : T1, not the contravariant constraint I specified in Constrained. You could do this with type constraints extension methods over Constrained, but you have to specify a lot of type parameters.
Ultimately, covariance/contravariance will probably partition the set of constrained function abstractions. For instance, a set of reusable function abstractions with type constraints would look something like this:
// covariant function types public abstract class Covariant<TReturn> { public abstract TReturn Apply(); } public abstract class Covariant<TArg0, TReturn> { public abstract TReturn Apply<T0>(T0 arg0) where T0 : TArg0; } public abstract class Covariant<TArg0, TArg1, TReturn> { public abstract TReturn Apply<T0, T1>(T0 arg0, T1 arg1) where T0 : TArg0 where T1 : TArg1; } // contravariant function types public abstract class Contravariant<TReturn> { public abstract T Apply<T>() where T : TReturn; } public abstract class Contravariant<TArg0, TReturn> { public abstract T1 Apply<T0, T1>(T0 arg0) where T0 : TArg0 where T1 :TReturn; } public abstract class Contravariant<TArg0, TArg1, TReturn> { public abstract T2 Apply<T0, T1, T2>(T0 arg0, T1 arg1) where T0 : TArg0 where T1 : TArg1 where T2 : TReturn; }
But even this isn't quite enough to properly type System.Enum.GetValues. GetValues is covariant but its method type argument has class-level type parameter dependency:
public abstract class TypeIndexedCovariant<TIndex, TReturn> { public abstract TReturn Apply<T0>() where T0 : TIndex where TReturn : IEnumerable<T0>; }
Of course, this isn't legal C# because you can't specify a class-type constraint on a method type parameter. I'm afraid these scenarios will require custom base class implementations for the near future. So we can implement a special base class to type System.Enum.GetValues like so:
public abstract class IndexedCovariant<TIndex> { public abstract IEnumerable<T0> Apply<T0>() where T0 : TIndex; } class EnumValues : IndexedCovariant<Enum> { public override IEnumerable<T0> Apply<T0>() { return Enum.GetValues(typeof(T0)) as T0[]; } }
In summary, a rewrite of my Sasa.Enums library in pure C# with Enum type constraints and no IL rewriting would look something like the following, assuming the above implementations for values and parsing:
public class EnumNames : IndexedCovariant<Enum, IEnumerable<string>> { public override IEnumerable<string> Apply<T0>() { return Enum.GetNames(typeof(T0)); } } // type-safe enum operations public static class Enums { public static readonly EnumParser Parse; public static readonly EnumValues Values; public static readonly EnumNames Names; } ... var names = Enums.Names.Apply<TypeCode>(); var values = Enums.Values.Apply<TypeCode>(); TypeCode parsed = Enums.Parse.Apply<TypeCode>("UInt16");
Not quite as pretty as the current approach in Sasa, but I could see a language on top of the CLR exploiting these sorts of base classes to support first-class functions with first-class polymorphism, ie. where the function argument constraints are just System.Object.
Edit: it's come to my attention that my explanations sometimes suck, so here's a TLDR version I posted to reddit:
The post was about exploring some options to enforce type constraints that C# normally forbids you from specifying, like constraints on Enum and Delegate in a way the C# compiler accepts and checks for you. This is done by encoding functions as objects and lifting the type constraints to class level type parameters, and enforcing method parameters to be subtypes of those parameters. Subclassing such an abstract type lets you specify Enum and Delegate constraints at the class level, and the method parameter constraints are inherited from the base class. Later in the post was a brief discussion of a few reusable abstractions for covariant and contravariant "function objects" of this form.Hopefully that clarifies a little!
Comments