Generic Math Part 3: Making an Interface with Operators
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