It's dangerous to code alone! Take this.

Generic Math Part 3: Making an Interface with Operators

Published on 19 Oct 2022.

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

This is the third post in a series about generic math. We’ve been working on a toy problem to make this Add method work with all sorts of different types:

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

It is working as expected, and we made our Point class implement IAddable<T>, but the built-in numeric types like float, double, and int do not implement our IAddable<T> interface and cannot be modified to support it.

That’s the issue we’ll address here.

Using an Adapter

In these situations, one possibility is the Adapter Pattern. The Adapter Pattern is a tool that can be used anytime you have a square peg, but need a round hole. When an object has one interface but you need it to look like another and can’t (or don’t want to) change it directly.

It requires making an extra class and an extra object to adapt the desired interface to the interface presented by the object it is adapting.

So we could, for example, make this class:

public class AddableFloat : IAddable<AddableFloat>
{
    public float Value { get; }
    public AddableFloat(float value) { Value = value; }
    public static AddableFloat operator +(AddableFloat a, AddableFloat b)
    {
        return new AddableFloat(a.Value + b.Value);
    }
}

Now we can call our Add method with AddableFloat objects instead of float, though we’ll have to convert to AddableFloat before and back to float after:

float sum = Add(new AddableFloat(2), new AddableFloat(4), new AddableFloat(-2)).Value;

The Adapter Pattern is a useful general-purpose pattern. If we had to, we could make this work. (And with some effort, we could make this AddableFloat type nicer.)

Fortunately, this is not the only way to solve this problem, and not the one we’ll spend the rest of our time pursuing. But it can be a useful tool in other similar situations.

The BCL’s Generic Math Interfaces

The main problem we have is that we need to constrain our generic method to only working with types that implement a specific interface. But we can’t just make our own interface and then apply it to existing types like float and int. But what if those types already implemented an interface that included a definition for the + operator?

Turns out, there’s a whole pile of interfaces that do exactly this!

These interfaces all live in the System.Numerics namespace, and if you’re doing much with them, you’ll probably want to include this at the top of your files:

using System.Numerics;

There is a giant pile of these interfaces, and we’ll talk about them in a bit more depth in the next post. But for now, there’s really only one that we need, and that is the interface IAdditionOperators, which is defined something like this:

public interface IAdditionOperators<TSelf, TOther, TResult> where TSelf : IAdditionOperators<TSelf, TOther, TResult>
{
    static abstract TResult operator +(TSelf left, TOther right);
}

They had the same issue we had when trying to ensure that an operator is only defined in a type that it is actually used in. That’s why they’ve got the where TSelf : IAdditionOperators<TSelf, TOther, TResult> at the end of the first line.

Our IAddable<T> interface assumed that both operands and the return type were all the same, while this version uses a different generic type parameter for each of those, which makes it possible to account for add operators that mix types. That comes at the cost of three generic type parameters. But, in theory, it means there’s no necessity to ever define your own interface that simply expects addition of any types, because IAdditionOperators should cover you.

All of the built-in number types implement this interface. For example, int implements IAdditionOperators<int, int, int>, meaning it has a defined operator for adding two int values to get a third int value. Meanwhile, float implements IAdditionOperators<float, float, float>.

Instead of defining our own interface, we could simply use IAdditionOperators and make our Point class also implement that interface.

Our Point class becomes this:

public class Point : IAdditionOperators<Point, Point, 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);
}

The actual operator definition remains identical, but the interface Point implements changed.

One important question, at this point, is whether it is better to make your own operator interfaces or use the ones in System.Numerics. This feature is new enough that I’m not confident in anybody’s claims of “best practices” just yet.

I do think there’s likely a time and place for each. Using the ones in System.Numerics means you don’t need to actually define an interface. And it means any method in the Base Class Library that expects one of those interfaces will be able to work for your type as well, as long as your type implements that interface. On the other hand, IAdditionOperators<Point, Point, Point> is more cumbersome than IAddable<T>. I think either pathway could be reasonable, depending on what, exactly, you’re trying to do, though I think I’d probably have a bias toward the ones in System.Numerics, if there isn’t a compelling reason to do otherwise.

Lastly, our Add method needs an update:

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

Since Point now implements IAdditionOperators<Point, Point, Point>, we can call this with:

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

Since float implements IAdditionOperators<float, float, float> and int implements IAdditionOperators<int, int, int>, both of these work:

int intSum = Add<int>(1, 2, 3);
float floatSum = Add<float>(4f, 5f, 6.7f);

There are a lot of interfaces in System.Numerics beyond just IAdditionOperators. We’ll talk about what’s there in a bit more depth next time.

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