Preview Features in .NET 6 – Generic Math

Tanner

If you’ve ever wanted to use operators with generic types or thought that interfaces could be improved by supporting the ability to define static methods as part of their contract, then this blog post is for you. With .NET 6 we will be shipping a preview of the new generic math and static abstracts in interfaces features. These features are shipping in preview form so that we can get feedback from the community and build a more compelling feature overall. As such, they are not supported for use in a production environment in .NET 6. It is highly recommended that you try the feature out and provide feedback if there are scenarios or functionality you feel is missing or could otherwise be improved.

Requires preview features attribute

Central to everything else is the new RequiresPreviewFeatures attribute and corresponding analyzer. This attribute allows us to annotate new preview types and new preview members on existing types. With this capability, we can ship an unsupported preview feature inside a supported major release. The analyzer looks for types and members being consumed that have the RequiresPreviewFeatures attribute and will give a diagnostic if the consumer is not marked with RequiresPreviewFeatures itself. To provide flexibility in the scope of a preview feature, the attribute can be applied at the member, type, or assembly level.

Because preview features are not supported for use in production and the APIs will likely have breaking changes before becoming supported, you must opt-in to using them. The analyzer will produce build errors for any call sites that haven’t been opted-into preview feature usage. The analyzer is not available in .NET 6 Preview 7, but will be included in .NET 6 RC1.

Static Abstracts in Interfaces

C# is planning on introducing a new feature referred to as Static Abstracts in Interfaces. As the name indicates, this means you can now declare static abstract methods as part of an interface and implement them in the derived type. A simple but powerful example of this is in IParseable which is the counterpart to the existing IFormattable. Where IFormattable allows you to define a contract for generating a formatted string for a given type, IParseable allows you to define a contract for parsing a string to create a given type:

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

    static abstract bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out TSelf result);
}

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

    public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out Guid result)
    {
        /* Implementation */
    }
}

A quick overview of the feature is:

  • You can now declare interface members that are simultaneously static and abstract
  • These members do not currently support Default Interface Methods (DIMs) and so static and virtual is not a valid combination
  • This functionality is only available to interfaces, it is not available to other types such as abstract class
  • These members are not accessible via the interface, that is IParseable<Guid>.Parse(someString, null) will result in a compilation error

To elaborate on the last point, normally abstract or virtual members are invoked via some kind of virtual dispatch. For static methods we don’t have any object or instance in which to carry around the relevant state for true virtual dispatch and so the runtime wouldn’t be able to determine that IParseable<Guid>.Parse(...) should resolve to Guid.Parse. In order for this to work, we need to specify the actual type somewhere and that is achievable through generics:

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

By using generics in the fashion above, the runtime is able to determine which Parse method should be resolved by looking it up on the concrete T that is used. If a user specified InvariantParse<int>(someString) it would resolve to the parse method on System.Int32, if they specified InvariantParse<Guid>(someString) it would resolve to that on System.Guid, and so on. This general pattern is sometimes referred to as the Curiously Recurring Template Pattern (CRTP) and is key to allowing the feature to work.

More details on the runtime changes made to support the feature can be found here.

Generic Math

One long requested feature in .NET is the ability to use operators on generic types. Using static abstracts in interfaces and the new interfaces being exposed in .NET, you can now write this code:

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

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

    return result;
}

public static TResult Average<T, TResult>(IEnumerable<T> values)
    where T : INumber<T>
    where TResult : INumber<TResult>
{
    TResult sum = Sum<T, TResult>(values);
    return TResult.Create(sum) / TResult.Create(values.Count());
}

public static TResult StandardDeviation<T, TResult>(IEnumerable<T> values)
    where T : INumber<T>
    where TResult : IFloatingPoint<TResult>
{
    TResult standardDeviation = TResult.Zero;

    if (values.Any())
    {
        TResult average = Average<T, TResult>(values);
        TResult sum = Sum<TResult, TResult>(values.Select((value) => {
            var deviation = TResult.Create(value) - average;
            return deviation * deviation;
        }));
        standardDeviation = TResult.Sqrt(sum / TResult.Create(values.Count() - 1));
    }

    return standardDeviation;
}

This is made possible by exposing several new static abstract interfaces which correspond to the various operators available to the language and by providing a few other interfaces representing common functionality such as parsing or handling number, integer, and floating-point types. The interfaces were designed for extensibility and reusability and so typically represent single operators or properties. They explicitly do not pair operations such as multiplication and division since that is not correct for all types. For example, Matrix4x4 * Matrix4x4 is valid, Matrix4x4 / Matrix4x4 is not. Likewise, they typically allow the input and result types to differ in order to support scenarios such as double = TimeSpan / TimeSpan or Vector4 = Vector4 * float.

If you’re interested to learn more about the interfaces we’re exposing, take a look at the design document which goes into more detail about what is exposed.

Operator Interface Name Summary
IParseable Parse(string, IFormatProvider)
ISpanParseable Parse(ReadOnlySpan<char>, IFormatProvider)
IAdditionOperators x + y
IBitwiseOperators x & y, x | y, x ^ y, and ~x
IComparisonOperators x < y, x > y, x <= y, and x >= y
IDecrementOperators --x and x--
IDivisionOperators x / y
IEqualityOperators x == y and x != y
IIncrementOperators ++x and x++
IModulusOperators x % y
IMultiplyOperators x * y
IShiftOperators x << y and x >> y
ISubtractionOperators x - y
IUnaryNegationOperators -x
IUnaryPlusOperators +x
IAdditiveIdentity (x + T.AdditiveIdentity) == x
IMinMaxValue T.MinValue and T.MaxValue
IMultiplicativeIdentity (x * T.MultiplicativeIdentity) == x
IBinaryFloatingPoint Members common to binary floating-point types
IBinaryInteger Members common to binary integer types
IBinaryNumber Members common to binary number types
IFloatingPoint Members common to floating-point types
INumber Members common to number types
ISignedNumber Members common to signed number types
IUnsignedNumber Members common to unsigned number types

The binary floating-point types are System.Double (double), System.Half, and System.Single (float). The binary-integer types are System.Byte (byte), System.Int16 (short), System.Int32 (int), System.Int64 (long), System.IntPtr (nint), System.SByte (sbyte), System.UInt16 (ushort), System.UInt32 (uint), System.UInt64 (ulong), and System.UIntPtr (nuint). Several of the above interfaces are also implemented by various other types including System.Char, System.DateOnly, System.DateTime, System.DateTimeOffset, System.Decimal, System.Guid, System.TimeOnly, and System.TimeSpan.

Since this feature is in preview, there are various aspects that are still in flight and that may change before the next preview or when the feature officially ships. For example, we will likely be changing the name of INumber<TSelf>.Create to INumber<TSelf>.CreateChecked and INumber<TSelf>.CreateSaturating to INumber<TSelf>.CreateClamped based on feedback already received. We may also expose new or additional concepts such as IConvertible<TSelf> or interfaces to support vector types and operations.

If any of the above or any other features are important to you or you feel may impact the usability of the feature in your own code, please do provide feedback (.NET Runtime or Libraries, C# Language, and C# Compiler are generally good choices). In particular:

  • Checked operators are not currently possible and so checked(x + y) will not detect overflow: csharplang#4665
  • There is no easy way to go from a signed type to an unsigned type, or vice versa, and so selecting logical (unsigned) vs arithmetic (signed) shift is not possible: csharplang#4682
  • Shifting requires the right-hand side to be System.Int32 and so additional conversions may be required: csharplang#4666
  • All APIs are currently explicitly implemented, many of these will likely become implicitly available on the types when the feature ships

Trying out the features

In order to try out the features there are a few steps required:

  1. Create a new C# console application targeting .NET 6 on the command line or in your favorite IDE

create a new project, Preview Features in .NET 6 – Generic Math

c# console application, Preview Features in .NET 6 – Generic Math

configure your new project, Preview Features in .NET 6 – Generic Math

additional information

  1. Edit the project file to opt into using preview features by setting the EnablePreviewFeatures property to true, and to reference the System.Runtime.Experimental NuGet package.

edit project file, Preview Features in .NET 6 – Generic Math

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <EnablePreviewFeatures>true</EnablePreviewFeatures>
    <LangVersion>preview</LangVersion>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>

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

</Project>
  1. Create a generic type or method and constrain it to one of the new static abstract interfaces
// See https://aka.ms/new-console-template for more information

using System.Globalization;

static T Add<T>(T left, T right)
    where T : INumber<T>
{
    return left + right;
}

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

Console.Write("First number: ");
var left = ParseInvariant<float>(Console.ReadLine());

Console.Write("Second number: ");
var right = ParseInvariant<float>(Console.ReadLine());

Console.WriteLine($"Result: {Add(left, right)}");
  1. Run the program and observe the output

First number: 5

Second number: 3.14

Result: 8.14

Closing

While we only briefly covered the new types and gave a simple example of their usage, the potential applications are much broader. We are looking forward to your feedback and seeing what awesome ways you can use this to improve your existing code or in the creation of new code. You can log feedback on any of the existing issues linked above or open new issues, as appropriate, on the relevant GitHub repository (.NET Runtime or Libraries, C# Language, and C# Compiler are generally good choices).

54 comments

Comments are closed. Login to edit/delete your existing comments

  • Matthew Arp

    One thing I don’t really understand about MSFT’s approach here is leaving pushing this out to the community as a ‘preview feature’. In my mind that basically nixes any use of it in any business development. So what real world non-production exposure is MSFT expecting the community to provide this preview feature? Don’t get me wrong I LOVE THIS. I do think that adding inheritance muddies the water. A generic T always and forever represents a single type and that statics are tied directly to the type, unless all statics are going to apply inheritance no statics should.

    • Tanner Gooding Microsoft employee

      We regularly push out previews for upcoming .NET features or releases. Doing so gives the community the opportunity to provide early feedback and influence the design if there are things that meaningfully impact their usage of it. The same goes for doing the design work in the open on GitHub and live streaming the API Reviews every week.

      Some features just take longer to bake than others and this is one of them. This has been a long requested feature in .NET, going back to when generics were first released and its not something that’s “easy” to get right. We certainly could have rushed out something “final” in .NET 6 or even kept it hidden/secret until .NET 7 or some future version when it is “ready”, but that would have blocked out valuable feedback and would not have given interested parties the opportunity to try this out first hand.

      We’re already getting valuable feedback and input on the feature and I expect that we will continue getting valuable feedback as users think of how they could use it in their own code and begin trying it out and writing their own preview logic. Their will likely be tweaks on both sides throughout the process and changes required, but the overall concept ideally doesn’t change too much.

      I do think that adding inheritance muddies the water. A generic T always and forever represents a single type and that statics are tied directly to the type, unless all statics are going to apply inheritance no statics should.

      Could you elaborate on this as I’m not quite sure I’m capturing what you mean?

      The functionality needs to be represented in the type system and exposed in a manner that allows other languages to consume the feature. Interfaces are a “natural” way to represent a shared contract that isn’t tied to a particular type. Likewise, generics are a way that allows a concrete type to be provided by the consumer of a type or API, allowing better code generation and reduced abstraction costs.

      • Matthew Arp

        I guess my point is that this seems like an instance were a good feature is being held back in preview in the vein of building the perfect feature. Generics weren’t created in a single release, they evolved over time, keeping static abstract under wraps till at least .NET 7 just seems like needless waiting.

        My point was that extending static abstract to abstract classes/inheritance seems counterintuitive in that static methods have always been directly referenced by using the type. With the current proposal that still holds true (even though that type has been genericized), if the feature were to be expanded to abstract classes that would not be the case.

        public interface ISomeType {
            static abstract string Value();
        }
        
        public class SomeType : ISomeType {
            public static string Value() => "Hello World";   
        }
        
        public abstract class BaseOtherType
        {
            static abstract string Value();
        }
        
        public class OtherType : BaseOtherType
        {
            public override static string Value() => "Not the mama";
        }
        
        public class AnotherTpe : OtherType
        {
        
        }
        
        public string DoSomeThing() {
            return SomeType.Value(); // Currently we can only access static methods by directly referencing the containing type
        }
        
        public string DoSomeThing<T>() where T : ISomeType {
            return T.Value(); // this makes sense because we know that whatever T is, that it will contain the static implementation of Value()
            //If we were to replace T with the typename of T this would still be valid (i.e. T.Value() -> SomeType.Value())
        }
        
        public string DoOtherThing<T>() where T : BaseOtherType {
            return T.Value(); // In my mind this breaks how statics work, Value() now could be somewhere on the type's hierarchy
            //If we were to replace T with the typename of T this may or may not be valid (i.e. T.Value() -> AnotherType.Value())
        }
        
        void Main() {
            DoSomeThing<SomeType>();
            DoOtherThing<AnotherType>(); // Does AnotherType.Value() make sense?
        }
        
  • Allan Lindqvist

    This is really cool!

    How does this relate to the case where you just have a type object and a string lets say?
    Since this is a compiler feature (mostly?) i guess you’d still do MakeGenericType on some other generic method with your type object and pass the value in there?

    • Tanner Gooding Microsoft employee

      The method exposed on the generic interface is abstract and not directly callable. Any method exposed which uses the generic interface as a constraint is just a standard generic method and should be invokable via reflection.

  • Maik Lathan

    The section “A quick overview of the feature is” states “You can now declare interface members that are simultaneously static and abstract”. I created a small test project like shown in this article. Attention on Console Application, net 6 preview 7, System.Runtime.Experimental, EnablePreviewFeatures = true and LangVersion = preview.

    The code:

    Console.WriteLine("Hello, World!");
    
    public interface IFoo
    {
        static abstract bool Test(string text);
    }
    

    After compilation I got the following error:

    error CS0112: A static member 'IFoo.Test(string)' cannot be marked as override, virtual, or abstract

    What is the correct way to declare interface members static and abstract simultaneously?

    • Tanner Gooding Microsoft employee

      Provided you have .NET 6 Preview 7 installed, are referencing the System.Runtime.Experimental NuGet package, and have EnablePreviewFeatures=true/LangVersion=preview set everything should work.

      It may be possible that you additionally need to ensure Visual Studio is on the latest preview or that Use previews of the .NET SDK needs to be checked in your Visual Studio options (Tools->Options->Environemtn->Preview Features`).

  • Andriy Savin

    I’m excited with this feature! But I can’t not agree with others that at least some interfaces should not be inherited by INumber. In particular, with parsing/formatting functionality this is a great example of Interface Segregation Principle. I’m pretty sure that it will be hard to find an example of generic code (meaning some common algorithm over numbers) which will need to use math operators and parsing/formatting at the same time. And in rare cases when this is needed it will be pretty ok to include several interface constraints in such a method.

  • silkfire

    You’re saying that:

    For static methods we don’t have any object or instance in which to carry around the relevant state for true virtual dispatch and so the runtime wouldn’t be able to determine that IParseable<Guid>.Parse(...) should resolve to Guid.Parse.

    But isn’t all the information you need right there? IParseable<Guid> should clearly resolve to Guid and nothing else.

  • Daniel Bower

    I’ve been waiting for this for so long 🙂

    Will it be possible to write something like

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

    or will we have to write

    public static TResult Add<T, TResult>(T a, T b)
        where T : INumber
        where TResult : INumber
    {
        return TResult.Create(a) + TResult.Create(b);
    }
    • Tanner Gooding Microsoft employee

      Yes. You only need T and TResult when you want to support the input and output types differing, such as if you want to take in integers and output floating-point.

  • JASON BOCK

    I was playing with the feature tonight, and I noticed that BigInteger doesn’t seem to have the interfaces to participate in generic math functions, such as the Add() example given in the article. Are there plans to update BigInteger such that it implements INumber, IAdditionOperators, etc.?

  • Mitchell Wheeler

    This is a long overdue, but amazing step forward!

    Looks like I can drop a solid 6,000+ lines of crazy generic dispatch systems for implementing add/sub/div/mul/mod/etc generic functions for the generic math we do in our project (and the almost as large static analyzer we’ve made to ensure they’re optimised away / the compiler plays nice)