Preview Features in .NET 6 – Generic Math
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
andabstract
- These members do not currently support Default Interface Methods (DIMs) and so
static
andvirtual
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:
- Create a new C# console application targeting .NET 6 on the command line or in your favorite IDE
- 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.
<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>
- 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)}");
- 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
Beautiful work Tanner, very excited for these features.
Ty Tanner and the entire team for keeping C# evolving.
Long waiting feature,thank you!
Great, it’s something I’ve wanted since the beginning! It’s not clear whether user-defined operators work with these in generic code. Your code samples show operators but maybe that’s just new syntactic sugar over these interfaces. Hopefully one day all operators can be abstracted out in this fashion.
Is there any performance penalty to calling something like ParseInvariant, other than the standard overhead of dispatching into a generic method? That is, does the CLR generate efficient inline code for each primitive type?
One of the last remaining pain points that often requires code generation is abstracting over constructors. The CLR missed a great early opportunity to simplify the design over the JVM: just make constructors static methods instead of a distinct kind of method, and permit static interface methods, and there you have constructor abstraction.
Yes
Nope, they introduced virtual static method dispatch on interfaces from CoreCLR. It’s not syntactic sugar.
Yes, but sometimes requires profile guided optimization to kick in
That’s not possible, a base class can do have constructors that derived classes don’t have. It cannot be abstracted using static interface methods.
While you can’t directly abstract constructors, you will be able to create “factory like” patterns. For example, something like the following is now possible:
You could imagine this naturally being extended to take additional parameters, similar to
Func<T>
(e.g.ICreatable<TSelf, TArg>
,ICreatable<TSelf, TArg1, TArg2>
, etc).And, of course, you get all the regular “benefits” of interfaces in that you can explicitly implement them and similar, so they aren’t part of your “normal” API surface
This is great, it finally allows us to do something more than the new() generic constraint.
My point was that abstraction is a core requirement of programming. If you create a concept or make choices that resist or inhibit abstraction, you should reconsider those choices.
The baggage of constructors that you point out is valid, but it hinges on the inheritance rules of static methods when playing the role as constructors. If constructors become a certain kind of static method, that method kind can follow the constructor inheritance rules rather than the standard rules for static methods.
This permits abstraction using the same mechanism discussed in this post, and eliminates some of the arbitrary distinctions between methods and constructors.
Welcomed feature, however “static abstract” in interfaces is the best everyone could agree on?
Slowly I don’t know any longer if I am on C# or C++, I hope that until final release there is a keyword to represent “static abstract” concept.
I’d recommend filing feedback for the language team at https://github.com/dotnet/csharplang.
Notably however,
static
andabstract
(and potentiallyvirtual
in the future) are all existing concepts with decently well understood meaning. They are also true keywords, not contextual, and so are unambiguous for the compiler to use. This is effectively just combining the two well known concepts and allowing them to be specified together.I dont understand the need for the abstract keyword. Non-static methods don’t require the abstract keyword, so why should static methods require it?
This was covered by Jared Parsons, the C# Compiler Dev Manager, on twitter and pasted to the general issue tracking the static abstracts in interfaces feature here: https://github.com/dotnet/csharplang/issues/4436#issuecomment-896912763
Effectively, static members have been legal in C# on interfaces since 8.0, legal in VB since the beginning. So there needs to be some syntax to disambiguate here.
The implementation details are surfacing a bit on the name.
The beautiful part of this implementation is that it essentially reused everything that was already in place, and the actual implementation for this capability was mostly about removing checks that existed in the VM (“This scenario does not make sense, so do not allow it”). It turns out that it did make sense to support this particular scenario, for precisely this work.
This feature essentially composed very well with what already existed. The first prototype removed 2 or 3 checks, and everything worked like magic. The real work was in the compiler, where new capabilities were allowed.
We originally gave it that name purely because that is how the sausage got made, basically, we ended up calling this feature “boiled sausages” instead of hot-dogs, because that is what we were doing: boiling sausages. It might be possible that another name would be more useful – I think the implementation details and the specific bit flipped will be erased from history, it just composes well with the existing idioms and is easy to reason about “static and abstract means you have to implement a static method in a class implementing it”.
This feature is really interesting and I look forward to investigating F# support. This could add a lot of consistency and efficiency as a replacement to current generic arithmetic workarounds and then be a good implementation of typeclasses.
I do hope that you will delete `INumber` before this feature gets released. It is a very silly interface, with things that restrict it to integers (`IIncrementOperators`, `IDecrementOperators`), stuff to do with formatting and parsing bunged in (`ISpanFormattable`…),
IUnaryPlusOperators
which do nothing, andCreate
methods which assume that any number is convertible to any other number, which is just not possible. If this type gets into .Net it will lower the intelligence of a lot of programmers.Could you elaborate a bit more on the potential issues you see?
Increment and Decrement are also valid operations for floating-point values and most numbers where
x + 1
is valid.Parsing and formatting are generally good to implement on primitives and simple data like types. It supports display and basic serialization/deserialization to things like the console, debugger, or file streams.
Yes, but many codebases do use these for code alignment or additional clarity for literals.
While you certainly can’t convert any type to any other type, there needs to be an API that allows conversion “generically”, otherwise you get stuck with specifying many different constraints which will hurt perf and usability in various scenarios. Exposing a
IConvertibleTo<TSelf, TFrom>
and/orIConvertibleFrom<TSelf, TFrom>
interface for every valid conversion will result in a massive explosion of metadata, particularly for the primitive types where each type is convertible to/from at least 13 other types.Let’s stand back. I would ask the question what does INumber represent? And then make sure that INumber expresses that, expresses nothing else, and that existing types and user-defined types that are numbers by the intended definition can satisfy INumber. Do you agree?
If INumber is just a way to collect existing functionality of some .Net numeric types, including quirks and hacks, without being usable outside, then it will be a significant failure. Do you agree? You would then have to call it ISystemNumericsNumber.
Parsing and formatting: obviously useful sometimes but with no connection to a Number. Someone who wants to implement a type supporting
INumber
should have no need to implement parsing and formatting operations. Just remove it from INumber and use an IFormattable requirement when needed.IConvertibleTo
is not possible to implement as an implementing class cannot know all possible classes which that have and will be created that supportINumber
and will not be able to convert all of them even if they were statically known. Why do you need any interface? This is just totally unsound.op_Implicit
while dangerous is at least sound. If this is retained then you need some way to restrict the supporting types to the 13 or so types you have in mind, preventing other types from implementingINumber
, and then the utility is massively reduced.Unary plus: doesn’t exist in mathematics. So it allows certain sorts of formatting in C#. That is a very weak reason to make it an INumber. Better to either remove the ability to format that way (making C# formatting conform to stadard mathematical notation), or have a separate interface ICanBeFormattedWithUnaryPlusInCSharp which types don’t need to implement to satisfy INumber.
Adding 1: not useful for floating points and not an unambiguous meaning of “increment”, which could also mean finding the next largest number, but this is a minor point compared to the above.
I agree with the point about splitting INumber.
I think it might be a good idea separating INumber into IFormattable and INumber, and so for deriving interfaces of INumber (IBinaryNumber) for example.
INumber combines two functionalities which can be separated — a thing that will ease the implementation of that interface.
Most use cases for INumber can be separated into either needing pure INumber (operators) and IFormattable, needing both in the same scenario is rare (as far as I think).
I agree that
INumber
is not an ideal name; the intent is clearly “any of the number types we already have” and probably so users of the feature just have to use that interface and they can use all the functionality of numeric types. It would be much less attractive if users had to specify 3-5 interfaces on their generic constraints.But from your point of view, as someone who wants to implement
INumber
, I agree that there is too much stuff in there that has nothing to do with a mere “number”. Something like yourISystemNumber
,IBuiltInNumber
,IPredefinedNumberType
or so would better express the intent – andINumber
should just contain numeric/mathematic functions.Where I have to disagree:
Increment and Decrement operators have always worked on non-integer types like
double
. It would be more confusing to not have them in an interface when they are available on the actual types. But where I do agree with you, is that they shouldn’t be in the basicINumber
interface, but why not in theISystemNumber
one that represents all the built-in types.Unary plus: it’s the counterpart of unary minus, that’s why it wouldn’t bother me, even though it has no effect on integers and doubles. Devs that implement
INumber
won’t build their own integral numeric type, we already have those, they probably build something number-ish, and in such specific use cases an unary + might make sense. Point is, if you do not include it inINumber
, where do you? Only inISystemNumber
? Probably not what a lot of people expect.That was definitely not my intent here. The intent is to provide a contract for what is considered a general “scalar number”. That is a number that can be represented as a single value. I clarify this here as types like
vectors
orcomplex numbers
can’t clearly be included as they represent multiple values and require additional semantics or considerations from the programmatic point of view.If you are defining some scalar number like type, then this can be implemented by the user. That would include concepts like
BigInteger
,Int128
, or other similar number types. To that extent,IScalarNumber
might be a better name, but it might also be more confusing to the common consumption case.There is also a lot more to numbers than just “math”. From a type system and programmatic point of view, the ability to create a number (from user input or from some other number) and display that number to the user is very important. Most of the usability for the type goes away if you can no longer perform these simple operations and you can’t even reliably write something like LINQs
Sum
method without them.Could you provide an example of where you might have a custom type that would implement
INumber
but where exposing the concept ofincrement
/decrement
doesn’t make sense. In general (and at least for numbers), its largely just a simplification ofx += 1
.INumber
is based on the core functionally exposed and expected by consumers of scalar number types. There is a balance between providing too little vs too much, and I believe the current definition is at least decently close. It represents all the central functionality exposed by the built-in number types (such asSystem.Int32
andSystem.Double
) and supports fairly core scenarios for console, web, and gui applications alike.I’m not sure I’d agree here. Numbers have a textual representation and one of the core concepts for any number type is displaying it to the user and likewise being able to create a number based on user input.
In real world apps, most inputs come from outside the program. Sometimes that’s from the user, sometimes its from a file (such as CSV), and sometimes its from some external source like a database. Very often and particularly for non-primitive types, this input comes in the form of a string and must be parsed.
For
op_Implicit
(orop_Explicit
) andConvertTo
the only real difference is the syntax required to use them. Fundamentally, they are both methods taking an input and returning an output.While it is indeed not possible to convert a given type to any other type, the concept of creating a value has to be represented somehow and in a way that can be efficiently supported. There must be some mechanism through which you can at least attempt to convert
T
toU
and to fail if that conversion is not supported as otherwise, you can’t even create someT
using an integer literal.I’m not sure I’d agree here. The plus sign certainly exists in math and not just in programming. But in either case .NET and most programming languages/frameworks it means the identity of
x
. It is a trivial thing to support and is “best practice” to do so for completeness.I do agree it isn’t great that it means users may have to add
operator T +(T value) => value
to their types if they don’t have that exposed already. To that extent, while .NET 6 does not include support for default implementations of static abstract methods, we are looking at supporting that for the official release. If we do, it is very likely that we will have unary plus implemented by default to simply have the expected behavior and that should simplify this scenario greatly.It is still a supported concept and one with fairly well-defined behavior in .NET. With regards to number types, it functionally means adding
1
and provides a convenience over having to writex += 1
.The current INumber definition is way off the mark and languages like F# should have nothing to do with it. We would need reimplement all functionality of everything using INumber because this is an extreme of ugliness, unlike the static interface methods feature itself which is very elegant. I know of repos in C# that define number types and I’m pretty sure they will not like it either.
“Numbers have a textual representation”. Numbers types don’t need to define a textual representation, and may be used internally in a code base. If they do have textual representations there may be many textual representations not just one. For example a rational number may have a LaTeX or MathML textual representation, and a rational type may support neither, one, or both. Such representations will often be defined outside the type itself since the operations of parsing and formatting are separate to the type. Likewise, numbers will have zero or more binary representations, or any other representations in all sorts of sets, so why are strings given special status?
You really need to be open to taking feedback from mathematicians here or else the concept of “Number” in .Net is going to be a mess. And from programmers that will want to implement this interface.
Good examples of interfaces are the many collection interfaces in System.Collections.Generic and System.Collections.Immutable – they do a pretty good job of keeping to the essentials and don’t bung in conversions or parsing and formatting.
A sum method requires Zero and an addition operation. If LINQ requires anything more than that then raise a bug report with LINQ!
You just stated the difference and it’s essential. One method can fail because it’s not sound and doesn’t conform to any sort of type safety. It should come as no surprise that some people are using .Net, rather than dynamic languages, because they like the safety that comes from static type systems. If INumber breaks that contract since it may fail because of incompatible types.
The plus sign exists in maths but not as a unary operator, since there is no purpose of defining an additional identiy operator. If it’s auto-provided then it will be less of a problem. Similarly if increment is auto-provided (using One and Plus) then that will remove an annoyance.
A natural comparison operator is the only thing that complex numbers don’t have here. They would usually be considered “scalar numbers”. Taking only the mathematical part of INumber, you have pretty much defined a field, with the nuance that integers are included since they define a division operator that executes a modulus calculation instead of division, and it’s a nice aspect of static interface methods that you can define IField.
If you define a clean INumber type, that does not prevent you from implementing other interfaces on the primitive .Net number types that you manage, such as IFormattable, and also doesn’t prevent you from defining combinations of interfaces, such as ISystemNumericsNumber which requires INumber and IFormatable and IUnsafeConvertibleFrom etc.. Why wouldn’t that keep everyone happy?
I have logged a comment on the overall design doc for this to be discussed more in API review: https://github.com/dotnet/designs/pull/205#issuecomment-898657150
First: This whole new feature looks amazing, I wanted something like this for a very long time and it’s nice to see that I can implement the things with C# how I always had in my mind but felt “restricted” by the language and had to use workarounds before. I did not vision this particular implementation with static interface methods, but it seems like reasonable solution. Good job!
But I kinda have to agree with these comments about the use case in the generic math examples, especially combining (I)Numbers with formatting, parsing and also Create/Convert itself from other types seems a bit awkward and too much work for just a number class.
Sadly I don’t have any good solutions, but I can only ask you to please reconsider these design choices with your team again and maybe you will find a different solution if you talk about it again.
Hallelujah!
Can’t figure out how to call an explicit implementation of static interface member (as implemented for primitives).
I want to call for example: double.Create(), but can’t figure out how.
They are explicitly implemented in .NET 6 and we will look at making certain APIs implicitly implemented in a future release of .NET.
In general, you access these members via generics:
This is great and something I’ve been looking forward to for a long time (coming from Objective-C which has had this with class-level as well as instance-level methods).
With this shipped, how far away is the similar
static abstract
members in class hierarchies? Being able to have a static factory method in a base class that is derived in the implementations would be great. Is the issue that, unless the class is also made generic in a similar way, it can’t refer to theSelf
type of the effective return value?I agree that
static abstract
sounds like a mouthful and also like a logical impossibility according to the original meaning ofstatic
, but it does explain how the members are resolved and where they live which comes in handy.static
is pretty much the only keyword in C# that refers to members that live on the type level instead of on the instance level.This is an area that we are looking into in the future. Implementing this required significant more work in the runtime though hence the initial preview was scoped to interfaces only.
Oh man, this is so cool. Echo @Sandro Magi’s comment – have been hoping for this for ages (Know Mads has too…). Can’t wait to dig in!
This looks really familiar. Almost like I designed it a year ago, wrote an article about it, and never got credit.
This was an effort primarily among .NET team members and I’m not aware of your prior project. Can you point to that article and design? It would be great to take a look at your approach.
Notably this was a culmination of past attempts by Microsoft (going back to 2010 and prior), recent investigation by the Xamarin team (January 2020: https://github.com/xamarin/partydonk), and various design work by the various teams starting in June 2020 when “partydonk” was presented in the C# Language Design Meeting (https://github.com/dotnet/csharplang/blob/3c8559f186d4c5df5d1299b0eaa4e139ae130ab6/meetings/2020/LDM-2020-06-29.md). There was also some influence based on what other languages are doing such as protocols in Swift and traits in Rust.
To add a little more color on the 2010 research.
Carol Eidt and some other folks had designed a solution that was almost the same solution that we ended up redesigning in January 2020, we were not aware at the time of the specific implementation details, but we did learn of her previous work, and were very happy that we were in alignment around the implementation specifics – which was a good validating point for the design.
The 2010 capability was not implemented at the time as there were other features competing for attention, and generic math had not risen to be more important.
Over time, that calculation changed, numerical applications are more common, and deep learning with its myriad of data types was not very pleasant to do in .NET without throwing away code. Once this rolls out, a lot of cumbersome code that we have can be vastly simplified and made more accessible to our users.
I assume this is the article you are referring to, Patrick? https://dev.to/entomy/generics-systems-83n
Thanks for writing it. It does a good job of covering the problem space and also provides good context from other programming languages. For folks that want a broader view on this topic to help better understand the C# solution, I recommend looking at Patrick’s article. I took away new insight from reading it.
For fairness, the article does not present a design, more a direction. If you do want to propose a direction or contribute a design (in full or in part), I recommend starting that as an issue on a dotnet repo. It’s fine for an issue to point to a blog post (like yours) as part of the context for a design. We’re just about to start .NET 7 planning. If you’ve got more ideas, now is a great time to engage.
Nope, different article.
And as for you, ever since we emailed two years ago I’ve been noticing remarkably similar code to what I’ve written show up in the product you manage, so you and I have nothing to speak about. MSFT did this with other projects numerous times, and it’s absolute BS.
Please share the article and same for the other instances. We take these sorts of claims very seriously. I’m assuming that the article must be public for someone to have copied it. If it is a public, sharing the link should be easy.
You could reasonably describe our design process as being too insular. That’s our bias. I do not believe — on the other end of the spectrum — that anyone on my team is representing the work of others as their own. Our design leaders would never tolerate that happening if they had knowledge of it. They are very rigorous.
Plagiarism is a serious claim in academic, commercial, or other copyright-oriented domains. I’ll not leave claims of improper attribution uninvestigated or uncontested. Given that you are raising these claims in a public forum, please back them up or I’ll have to consider them (and I’m sure others will as well) hollow. Otherwise, drop the claim if you cannot or will not substantiate it.
I think Patrick is referring to this article: https://dev.to/entomy/real-traits-in-c-4fpk, from September 2020, which is very clever. I saw it on Twitter too: https://twitter.com/pkell7/status/1425593504777089025?s=20
I suspect his concern is about the style used to propose the new INumber interface and how it resembles the idea in the traits article and the source code of his GitHub projects. So yes, he has a good point.
Hello Jorge,
We had the INumber design in early Feburary 2020, before this post you referenced from September 2020 (7 months later), but I just remembered an interesting historical nugget that I wanted to share.
When we started looking at this, and liked Swift’s associated type, in particular “Self”, which made expressing this problem a piece of cake. This took place in October 2019, just when I had freshly started on AI work (which was part of the inspiration for this work), we would create the first paper that described the goals and design by December 2019 that we shared with Mads, Jared and Jan for feedback.
Then a stroke of genius from Mads Torgersen set us in the right path, we did not know to implement the supporting capabilities of Associated Types, we could just get clever with the use of generics (these are the TSelf that you see in the original partydonk posts, a homage to the Swift-inspired design). What you see in the earlier Partydonk commits is already the
That really unlocked the doors, because we went from “We need design changes to the type system to get started” to just a handful of changes in the VM (mostly check removal, that I documented previously)
More prior art:
Also see ImmutableArray<T>: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableArray_1.cs
@Richard Lander, I’ve had generic operators in my Sasa library since 2012.
The current version is even more efficient, doesn’t need runtime compilation, and handles all operators with multiple possible overloads, ie. var baz = Operators<Foo, Bar, Baz>.Add(someFoo, someBar);
This version has been available publicly since 2013.
If you think you are the first to have had this “idea” (about a year ago even!) then you are fooling yourself. Things get reinvented independently all the time and this stuff has been on the radar of a lot of people for a long time.
I think it’s strange to want “credit” for concepts that have existed for over a decade. Scala has had numeric traits exactly like this in their standard library since 2009.