Generic Math in .NET 6

This post looks at Generic Math which is coming (in preview form) in .NET 6.

Generic Math (which roughly translated into English means "Generic Maths") is the ability to use arithmetic operators on generic types. To give a quick example of what problems it solves, see that this isn't currently possible in C#:

public T Add<T>(T left, TRight) => left + right;

Generic Math makes this possible through a set of new interfaces, with each interface representing a mathematical concept, such as addition, subtraction, multiplication, etc.

But these are not just any interfaces (no, they're not M&S interfaces); they rely on a new language feature called Static Abstracts in Interfaces, which we'll cover first in this post.

They're both available to try now as part of .NET 6 preview 7 (see the FAQ at the end for instructions).

We'll see what both of these are and why they're useful, starting with Static Abstracts in Interfaces.


Static Abstracts in Interfaces #

I must admit, I struggled to comprehend what these were when I first read about them. I had a Peter Kay (English comedian) 'Cheese, Cake?' moment!

The concept seemed strange to me. After all, an interface is already entirelymostly abstract, so why do we need an abstract keyword on a static method? Moreover, why have a static method on an interface when we can already define methods that implementers must, er, implement?

But it slowly became clear after I read through the documentation and the code. Here's an example of how they look (taken straight from the documentation):

public interface IParseable<TSelf> where TSelf : IParseable<TSelf>
{
static abstract TSelf Parse(string s, IFormatProvider? provider);
}

This declares that all types that implement this interface must also have that static method, e.g.

public readonly struct Guid : IParseable<Guid> {
public static Guid Parse(string s, IFormatProvider? provider) {
/* Implementation */
}
}

We can now use code like this to parse different types:

public static T InvariantParse<T>(string s) where T : IParseable<T>
{
return T.Parse(s, CultureInfo.InvariantCulture);
}

Generic Math makes heavy use of this new feature.

Generic Math #

The initial idea behind Generic Math was to define a new baseline for how developers write algorithms in .NET.

Why is it needed? Before C#'s Generics, there were C++'s Templates. Both are language features for parametrised types, but Generics are simpler to use and hence lack some of the functionality of Templates. There's quite a few differences between them, but the main one relating to how we currently write algorithms is (emphasis mine):

C# generics do not provide the same amount of flexibility as C++ templates. For example, it is not possible to call arithmetic operators in a C# generic class, although it is possible to call user defined operators.

So, what does it mean that it's not possible to call arithmetic operators in a C# generic class? Isn't that what Linq does? e.g. here's Sum (from Enumerable.cs in .NET):

public static int Sum(this IEnumerable<int> source) {
int sum = 0;
foreach (int v in source) {
sum += v;
}

return sum;
}

The key here is generic class. IEnumerable<int> is generic (a closed type), but the int type obviously isn't a generic type (an open type). This means that currently, we can't write a generic version like this:

public static T Sum<T>(this IEnumerable<T> source) {
T sum = default;
foreach (T v in source) {
sum += v;
}

return sum;
}

... ignoring line 2, if we tried to build that, we'd get: error CS0019: Operator '+=' cannot be applied to operands of type 'T' and 'T'.

Just to reiterate: the first Sum does work because it wasn't dealing with a generic class (int), hence arithmetic operators can be used, but the second Sum doesn't work because we currently can't use arithmetic operators on a generic class (T).

Sticking with the Sum example; because of the inability to call arithmetic operators on a generic class, we have to write specialised overloads of Sum for each supported type. Each overload has near identical code, resulting in lots of repetition. To demonstrate this repetition, here's some more snippets of the Enumerable class in .NET which contains the Sum# extension methods:

public static partial class Enumerable {
public static int Sum(this IEnumerable<int> source) {
...
public static long Sum(this IEnumerable<long> source) {
...
public static float Sum(this IEnumerable<float> source) {
...

Here, int, long, and float all have arithmetic operators, but we can't use them when they are used via a generic class.

Therefore, we need an alternative way of describing the abilities of generic types; things like methods and operators. This is exactly what Static Abstracts in Interfaces are for.

And now that we can describe methods and operators on generic classes, it's possible to create a set of interfaces to represent mathematical concepts such as addition, subtraction, division, etc. And this is exactly what Generic Math is.

Generic Math is a set of Static Abstracts in Interfaces. For example, anything that can be 'added' should implement IAdditionOperators which is part of Generic Math.

Here is the relevant bit of IAdditionOperators (the rest is here):

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

Another example is ISubtractionOperators #

 public interface ISubtractionOperators<TSelf, TOther, TResult>
where TSelf : ISubtractionOperators<TSelf, TOther, TResult>
{
static abstract TResult operator -(TSelf left, TOther right);

Other types include IMultiplyOperators, IComparisonOperators, etc. There's about 18 or so of them.

The native types in .NET 6 have been updated to implement these new interfaces. Here's the implementation of IAdditionOperators for int (Int32.cs): #

[RequiresPreviewFeatures]
static int IAdditionOperators<int, int, int>.operator +(int left, int right)
=> left + right;

With these new interfaces, the generic version of Sum is now possible:

static T Sum<T>(IEnumerable<T> values)
where T : INumber<T>
{
T result = T.Zero;

foreach (var value in values)
{
result += T.Create(value);
}

return result;
}

You might have spotted INumber<T># on line 2 above. This type bunches together multiple Generic Math interfaces associated with numbers. int and the other number types in .NET have been updated to implement this new interface (although not directly, they do so through ISignedNumber and IUnsignedNumber).

/// <summary>Defines a number type.</summary>
/// <typeparam name="TSelf">The type that implements the interface.</typeparam>
[RequiresPreviewFeatures]
public interface INumber<TSelf>
: IAdditionOperators<TSelf, TSelf, TSelf>,
IAdditiveIdentity<TSelf, TSelf>,
IComparisonOperators<TSelf, TSelf>, // implies IEquatableOperators<TSelf, TSelf>
IDecrementOperators<TSelf>,
IDivisionOperators<TSelf, TSelf, TSelf>,
IIncrementOperators<TSelf>,
IModulusOperators<TSelf, TSelf, TSelf>,
IMultiplicativeIdentity<TSelf, TSelf>,
IMultiplyOperators<TSelf, TSelf, TSelf>,
ISpanFormattable, // implies IFormattable
ISpanParseable<TSelf>, // implies IParseable<TSelf>
ISubtractionOperators<TSelf, TSelf, TSelf>,
IUnaryNegationOperators<TSelf, TSelf>,
IUnaryPlusOperators<TSelf, TSelf>
where TSelf : INumber<TSelf>

Why is it useful? #

There are two scenarios listed in the Designs Doc:

It's also not just for numbers. Other types in .NET 6 will use these features. Types like Span, DateTimeOffset, and char now implement things like IAdditionOperators.

Self Self Self! #

In the examples above, you might've spotted a lot of mentions of TSelf, e.g.

public interface IParseable<TSelf> where TSelf : IParseable<TSelf>
{
static abstract TSelf Parse(string s, IFormatProvider? provider);
}

This pattern is called the Curiously recurring template pattern. If you squint, it looks like the base class is deriving from the derived class! Even though this isn't a new pattern, it will likely be used a lot more from .NET 6 onwards.

This pattern could be problematic though. That's because there's an implicit relationship between the class and the interface, but this relationship cannot currently be expressed in C#.

If your type was IParseable, you'd have something like:

public class MyType : IParseable<MyType> ...

Because the relationship between the MyType and IParseable<MyType> cannot be expressed (or, rather, constrained), we need to be careful that they match correctly. Obviously, this won't be a problem for you and I, but apparently there are developers out there who copy & paste code, which may result in inadvertently pasting a type and then forgetting to change the relationship, e.g.

public class MegaThing : IParseable<MyType> ...

The ability to mess up this relationship has been seen as a problem by the .NET team, and some proposals have been made to make this expressible in C#, e.g.

public interface IParseable<this TSelf> { ... }
// -or-
public interface IParseable<TSelf>
where TSelf : this { ... }

// vs today
public interface IParseable<TSelf>
where TSelf : IParseable<TSelf> { ... }

This would solve two problems:

It is hoped that this feature will be part of C# 11 and .NET 7.

What have we seen? #

We looked at how Static Abstracts in Interfaces is a new C# language feature that allows new library features such as Generic Math. We've also seen that there's follow-on proposals for the C# language to add constraints for formalising the relationships in the Curiously recurring template pattern.

FAQ #

How do I try it out? #

Create a new console app (dotnet new console), then change your .csproj file so that it includes EnablePreviewFeatures and LangVersion; it should look something like this:

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<EnablePreviewFeatures>True</EnablePreviewFeatures>
<LangVersion>preview</LangVersion>
</PropertyGroup>

Next, add the System.Runtime.Experimental package (or just add this to your .csproj):

<ItemGroup>
<PackageReference Include="System.Runtime.Experimental" Version="6.0.0-preview.7.21377.19" />
</ItemGroup>

💡 Update - January 2022

If you're reading this and nothing works, then it could be down to a .NET update that breaks this feature. The offending version is .NET 6.0.101. One of the known workarounds is to add this global.json file:

{
"sdk": {
"version": "6.0.100",
"rollForward": "disable"
}
}

More info here: https://github.com/dotnet/runtime/issues/62840


Now, in program.cs, change it to look like this:

using System;
using System.Collections.Generic;

int result = Sum(new[] { 1, 2, 3 });
Console.WriteLine(result);

static T Sum<T>(IEnumerable<T> values)
where T : INumber<T>
{
T result = T.Zero;

foreach (var value in values)
{
result += T.Create(value);
}

return result;
}

Does Static Abstracts on Interfaces have a performance impact? #

There was a minor performance impact on startup once Generic Math was introduced, although work was done to optimise this, so it looks like the performance impact is negligible.

What runtime is required for Generic Math? #

.NET 6 is required, although it will be shipped as a preview feature when it ships in November '21 so as to gather feedback.

Is it a C# 10 only feature? #

Yes. And No. Static Abstracts in Interfaces are in 'preview' in C# 10. This means the feature could change, potentially in breaking ways, before it's finally released in its finished state as part of C# 11 (yes, 11!)

🙏🙏🙏

Since you've made it this far, sharing this article on your favorite social media network would be highly appreciated 💖! For feedback, please 🦋 ping me on Bluesky! 🦋

Leave a comment

Comments are moderated, so there may be a short delays before you see it.

3 comments on this page

  • Bill

    commented

    Great explanation. Thank you.

  • Steve Dunn

    commented

    Jason - apparently, this was broken in .NET 6.0.101. One of the known workarounds is to add this global.json file:

    {
    "sdk": {
    "version": "6.0.100",
    "rollForward": "disable"
    }
    }

    More info here: https://github.com/dotnet/runtime/issues/62840
  • Jason S. Clary

    commented

    Do you know if the requirements changed since .NET 6 was released? I can add the experimental package, EnablePreviewFeatures and LangVersion but I can't build due to complaints about the interfaces not being defined. They aren't in System, System.Numerics or System.Runtime and a System.Runtime.Experimental namespace doesn't exist. I've tried adding the package both with the specific preview version you and Tanner Gooding (on devblogs.microsoft.com) specify and just with 6.0.0 which is what you get if you use dotnet add package without a version. Tanner doesn't specify any using statements at all and you only seem to specify ones required for unrelated classes used in the examples which doesn't seem right unless it was previously in System. I'd have posted on Tanner's blog post but comments were closed at some point prior to release. I'm not finding any other posts about this feature besides yours and Tanner's.

    This is rather a shame as I've basically been waiting for this feature for about a decade. I do a lot of work with unusual numeric types and typically have to resort to C++ or jump through insane and inefficient hoops to make things work in C#. I'm really hoping this gets added to F# as well. At some point I may break down and write my own language since I also need user-defined infix operators to avoid a ton of long fluent method call chains that are hard to read and debug. Once you move beyond the limits of the Real numbers, there are at least four different types of multiplication (interior, exterior/cross, inner/dot and outer) and that's assuming you stick to euclidean space. Lots of other things get complicated in higher dimensions and other metric spaces.

Published