Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Q&A

Welcome to Software Development on Codidact!

Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.

Comments on F-bounded polymorphism, interface operators, and type inference in C#

Post

F-bounded polymorphism, interface operators, and type inference in C#

+5
−0

C# language version 8.0 introduces limited support for static methods, operators, etc. in interfaces. However, there are still limitations. I was hoping to use the new language features to try a generic approach to abstract algebra with proper operator overloading, but I'm running into the limitations.

Ideally what I'd like is

public interface IRing<TRing> where TRing : IRing<TRing>
{
    public static TRing operator -(TRing a);
    public static TRing operator +(TRing a, TRing b);
    public static TRing operator -(TRing a, TRing b) => a + -b;
    public static TRing operator *(TRing a, TRing b);
}

which can be used as e.g.

static TRing EvaluatePolynomial<TRing>(TRing x, params TRing[] coeffsBE)
    where TRing : IRing<TRing>
{
    var y = x - x; // Initially zero
    foreach (var coeff in coeffsBE)
        y = y * x + coeff;
    return y;
}

There are two obvious problems: static members of interfaces need implementations, and operator overloads must use the enclosing type. The first is easy enough to work around:

public interface IRing<TRing> where TRing : IRing<TRing>
{
    TRing Negate();
    TRing Add(TRing addend);
    TRing Mul(TRing addend);

    public static TRing operator -(TRing a) => a.Negate(b);
    public static TRing operator +(TRing a, TRing b) => a.Add(b);
    public static TRing operator -(TRing a, TRing b) => a + -b;
    public static TRing operator *(TRing a, TRing b) => a.Mul(b);
}

But the second is problematic. If I try changing the type of the first argument of each operator overload to IRing<TRing>:

public interface IRing<TRing> where TRing : IRing<TRing>
{
    TRing Negate();
    TRing Add(TRing addend);
    TRing Mul(TRing multiplicand);

    public static TRing operator -(IRing<TRing> a) => a.Negate();
    public static TRing operator +(IRing<TRing> a, TRing b) => a.Add(b);
    public static TRing operator -(IRing<TRing> a, TRing b) => a + -b;
    public static TRing operator *(IRing<TRing> a, TRing b) => a.Mul(b);
}

then the generic EvaluatePolynomial compiles, but operations on specific implementations don't necessarily. This simple implementation:

public struct BigInt : IRing<BigInt>
{
    private BigInt(BigInteger value) { Value = value; }

    public static implicit operator BigInt(int value) => new BigInt(value);

    private readonly BigInteger Value;

    public BigInt Negate() => new BigInt(-Value);
    public BigInt Add(BigInt addend) => new BigInt(Value + addend.Value);
    public BigInt Mul(BigInt multiplicand) => new BigInt(Value * multiplicand.Value);

    public static void Test()
    {
        BigInt one = 1;
        BigInt negOne = -one;
        var two = one + one;
    }
}

gives me errors on -one and one + one because, apparently, the type inference doesn't recognise that BigInt can be implicitly coerced to IRing<BigInt> and so match the signatures of the operator overloads.

Is there a straightforward solution to this which doesn't require reimplementing the operator overloads in each implementing class or explicitly coercing BigInt to IRing<BigInt>?

History
Why does this post require moderator attention?
You might want to add some details to your flag.
Why should this post be closed?

1 comment thread

General comments (3 comments)
General comments
elgonzo‭ wrote about 3 years ago · edited about 3 years ago

Well, if you want/need to stick with interfaces and structs, you are out of luck, as you already noticed - and which pretty much would be the answer to the question. (1/2)

elgonzo‭ wrote about 3 years ago

(2/2) However, if your problem behind your Q is not strictly requiring interfaces and structs, and if your application scenario is not particularly restricted/intolerant towards object allocations, you could turn the interface into an abstract class public abstract class RingBase<TRing> where TRing : RingBase<TRing> and the BigInt struct into a class inheriting the abstract class.

Peter Taylor‭ wrote about 3 years ago

@elgonzo, that's pretty much what I suspected. For my immediate purposes it's not strictly necessary to use an interface, but using an abstract class instead eliminates some interesting future directions. E.g. an orderable subtype and a field subtype couldn't be combined to an orderable field subtype because of the diamond problem.