Skip to main content

Circumventing C#'s Type Constraint Limitations

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:

  1. A general set of base classes won't have type constraints between method parameters, ie. like T0 : T1, which are sometimes needed.
  2. 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

Popular posts from this blog

async.h - asynchronous, stackless subroutines in C

The async/await idiom is becoming increasingly popular. The first widely used language to include it was C#, and it has now spread into JavaScript and Rust. Now C/C++ programmers don't have to feel left out, because async.h is a header-only library that brings async/await to C! Features: It's 100% portable C. It requires very little state (2 bytes). It's not dependent on an OS. It's a bit simpler to understand than protothreads because the async state is caller-saved rather than callee-saved. #include "async.h" struct async pt; struct timer timer; async example(struct async *pt) { async_begin(pt); while(1) { if(initiate_io()) { timer_start(&timer); await(io_completed() || timer_expired(&timer)); read_data(); } } async_end; } This library is basically a modified version of the idioms found in the Protothreads library by Adam Dunkels, so it's not truly ground bre

Building a Query DSL in C#

I recently built a REST API prototype where one of the endpoints accepted a string representing a filter to apply to a set of results. For instance, for entities with named properties "Foo" and "Bar", a string like "(Foo = 'some string') or (Bar > 99)" would filter out the results where either Bar is less than or equal to 99, or Foo is not "some string". This would translate pretty straightforwardly into a SQL query, but as a masochist I was set on using Google Datastore as the backend, which unfortunately has a limited filtering API : It does not support disjunctions, ie. "OR" clauses. It does not support filtering using inequalities on more than one property. It does not support a not-equal operation. So in this post, I will describe the design which achieves the following goals: A backend-agnostic querying API supporting arbitrary clauses, conjunctions ("AND"), and disjunctions ("OR"). Implemen

Easy Automatic Differentiation in C#

I've recently been researching optimization and automatic differentiation (AD) , and decided to take a crack at distilling its essence in C#. Note that automatic differentiation (AD) is different than numerical differentiation . Math.NET already provides excellent support for numerical differentiation . C# doesn't seem to have many options for automatic differentiation, consisting mainly of an F# library with an interop layer, or paid libraries . Neither of these are suitable for learning how AD works. So here's a simple C# implementation of AD that relies on only two things: C#'s operator overloading, and arrays to represent the derivatives, which I think makes it pretty easy to understand. It's not particularly efficient, but it's simple! See the "Optimizations" section at the end if you want a very efficient specialization of this technique. What is Automatic Differentiation? Simply put, automatic differentiation is a technique for calcu