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

Dashboard
Notifications
Mark all as read
Q&A

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

+4
−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>?

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

3 comments

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‭ 21 days 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. elgonzo‭ 21 days 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. Peter Taylor‭ 21 days ago

0 answers

Sign up to answer this question »