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#
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>
?
1 comment thread