It's dangerous to code alone! Take this.

Generic Math Part 2: Making an Interface with Operators

Published on 18 Oct 2022.

This is Part 2 in a series about generic math. You can view all parts here: Part 1 Part 2 Part 3 Part 4

In the previous post, we discussed the problem, which I’ll summarize again here. We could make this method to do a three-way addition:

public static float Add(float a, float b, float c)
{
    return a + b + c;
}

But we’d like to solve the problem for lots of types, not just float. We’d like it to work with int, double, and our own Point class.

While we could copy and paste and make similar Add methods for these other types, generics should allow us to make a single method that works on a wide variety of types.

The simplest solution is to simply make a generic method:

public static T Add<T>(T a, T b, T c) // COMPILER ERROR!
{
    return a + b + c;
}

But this doesn’t work because the compiler knows next to nothing about the type T, and it certainly can’t guarantee there is an addition operator on it, so we get a compiler error.

The typical solution here is to constrain the generic type parameter to something that has an addition operator. There is not a generic type constraint that lets you limit by available operators, but there is an option to constrain to specific interfaces, and we can put an operator in an interface.

So that’s the approach we’ll take.

Unfortunately, operators in interfaces is… complex. And ugly.

What would be nice to do is something like this:

public interface IAddable
{
    static abstract Point operator +(Point a, Point b);
}

That is actually valid syntax, but it isn’t what we want. It only supports adding two points (and could only be implemented by the Point type anyway).

Again, that’s what generics are for. We can enhance that code with generics. But again, the simple solution below isn’t quite good enough:

pulbic interface IAddable<T> // Compiler error!
{
    static abstract T operator +(T a, T b);
}

We’re much closer, but this version doesn’t even compile. The problem is that you cannot define an operator inside of a type unless one of the operands is the type in question. In this case, we’re defining an operator in a type called IAddable<T>, but the operands are both of type T!

It doesn’t work!

We’d need to ensure that T is also of the type IAddable. But we can actually do that with yet another constraint. The version below actually works:

public interface IAddable<T> where T : IAddable<T>
{
    static abstract T operator +(T a, T b);
}

That where T : IAddable<T> is mildly annoying, but it will ensure that any argument for T implements IAddable<T>.

The next thing to do is make our Point class implement this interface:

public class Point : IAddable<Point>
{
    public float X { get; }
    public float Y { get; }
    public Point(float x, float y) { X = x; Y = y; }
    public static Point operator +(Point a, Point b) =>
                           new Point(a.X + b.Y, a.Y + b.Y);
}

We already had the operator’s definition in there, but that version satisfies what is demanded by IAddable<Point>, so we should be good.

Now we can update our Add method to constrain it to using only types that implement IAddable<T>:

public static T Add<T>(T a, T b, T c) where T : IAddable<T>
{
    return a + b + c;
}

And now this method can be called like so:

Point sum = Add<Point>(new Point(1, 2), new Point(2, 1), new Point(-2, -2));

Unfortunately, it cannot be called with the float type. The float type does not implement IAddable<float>, and we don’t own the definition of float, so we cannot change it to implement it.

There’s a solution to this, which we’ll see next time!

This is Part 2 in a series about generic math. You can view all parts here: Part 1 Part 2 Part 3 Part 4