33

Is there any feasible way of using generics to create a Math library that does not depend on the base type chosen to store data?

In other words, let's assume I want to write a Fraction class. The fraction can be represented by two ints or two doubles or whatnot. The important thing is that the basic four arithmetic operations are well defined. So, I would like to be able to write Fraction<int> frac = new Fraction<int>(1,2) and/or Fraction<double> frac = new Fraction<double>(0.1, 1.0).

Unfortunately there is no interface representing the four basic operations (+,-,*,/). Has anybody found a workable, feasible way of implementing this?

1

6 Answers 6

34

Here is a way to abstract out the operators that is relatively painless.

    abstract class MathProvider<T>
    {
        public abstract T Divide(T a, T b);
        public abstract T Multiply(T a, T b);
        public abstract T Add(T a, T b);
        public abstract T Negate(T a);
        public virtual T Subtract(T a, T b)
        {
            return Add(a, Negate(b));
        }
    }

    class DoubleMathProvider : MathProvider<double>
    {
        public override double Divide(double a, double b)
        {
            return a / b;
        }

        public override double Multiply(double a, double b)
        {
            return a * b;
        }

        public override double Add(double a, double b)
        {
            return a + b;
        }

        public override double Negate(double a)
        {
            return -a;
        }
    }

    class IntMathProvider : MathProvider<int>
    {
        public override int Divide(int a, int b)
        {
            return a / b;
        }

        public override int Multiply(int a, int b)
        {
            return a * b;
        }

        public override int Add(int a, int b)
        {
            return a + b;
        }

        public override int Negate(int a)
        {
            return -a;
        }
    }

    class Fraction<T>
    {
        static MathProvider<T> _math;
        // Notice this is a type constructor.  It gets run the first time a
        // variable of a specific type is declared for use.
        // Having _math static reduces overhead.
        static Fraction()
        {
            // This part of the code might be cleaner by once
            // using reflection and finding all the implementors of
            // MathProvider and assigning the instance by the one that
            // matches T.
            if (typeof(T) == typeof(double))
                _math = new DoubleMathProvider() as MathProvider<T>;
            else if (typeof(T) == typeof(int))
                _math = new IntMathProvider() as MathProvider<T>;
            // ... assign other options here.

            if (_math == null)
                throw new InvalidOperationException(
                    "Type " + typeof(T).ToString() + " is not supported by Fraction.");
        }

        // Immutable impementations are better.
        public T Numerator { get; private set; }
        public T Denominator { get; private set; }

        public Fraction(T numerator, T denominator)
        {
            // We would want this to be reduced to simpilest terms.
            // For that we would need GCD, abs, and remainder operations
            // defined for each math provider.
            Numerator = numerator;
            Denominator = denominator;
        }

        public static Fraction<T> operator +(Fraction<T> a, Fraction<T> b)
        {
            return new Fraction<T>(
                _math.Add(
                  _math.Multiply(a.Numerator, b.Denominator),
                  _math.Multiply(b.Numerator, a.Denominator)),
                _math.Multiply(a.Denominator, b.Denominator));
        }

        public static Fraction<T> operator -(Fraction<T> a, Fraction<T> b)
        {
            return new Fraction<T>(
                _math.Subtract(
                  _math.Multiply(a.Numerator, b.Denominator),
                  _math.Multiply(b.Numerator, a.Denominator)),
                _math.Multiply(a.Denominator, b.Denominator));
        }

        public static Fraction<T> operator /(Fraction<T> a, Fraction<T> b)
        {
            return new Fraction<T>(
                _math.Multiply(a.Numerator, b.Denominator),
                _math.Multiply(a.Denominator, b.Numerator));
        }

        // ... other operators would follow.
    }

If you fail to implement a type that you use, you will get a failure at runtime instead of at compile time (that is bad). The definition of the MathProvider<T> implementations is always going to be the same (also bad). I would suggest that you just avoid doing this in C# and use F# or some other language better suited to this level of abstraction.

Edit: Fixed definitions of add and subtract for Fraction<T>. Another interesting and simple thing to do is implement a MathProvider that operates on an abstract syntax tree. This idea immediately points to doing things like automatic differentiation: http://conal.net/papers/beautiful-differentiation/

Sign up to request clarification or add additional context in comments.

5 Comments

In the general way I think MathProvider should be made into a interface and Subtract into a ordinary interface method or it could be implemented as an Extension Method. That would on the other hand disallow overriding it.
I wonder about the performance of your solution... It works great only if everything is inlined...
also you need to completely re-implement System.Math
Based on the answer of John D. Cook below ("division of int by int will be an integer division and produce wrong result"), you may want to add a public abstract double DivideToDouble (T a, T b)
@fryguybob can you show an example of how to use it
7

I believe this answers your question:

http://www.codeproject.com/KB/cs/genericnumerics.aspx

1 Comment

That solution, and others available (like using Emit) are not really clean at all, so that's not what I was looking for. But thanks anyways :)
4

Here's a subtle problem that comes with generic types. Suppose an algorithm involves division, say Gaussian elimination to solve a system of equations. If you pass in integers, you'll get a wrong answer because you'll carry out integer division. But if you pass in double arguments that happen have integer values, you'll get the right answer.

The same thing happens with square roots, as in Cholesky factorization. Factoring an integer matrix will go wrong, whereas factoring a matrix of doubles that happen to have integer values will be fine.

1 Comment

Arguably, integers are inherently wrong for such algorithms. The "correct" solution is unlikely to be an integer value. You'll see this with "double arguments that happen to have integer values" when the result isn't integer. In general, intermediate calculations should always be done with a real (float, double) type. But I agree it is worth being aware of.
3

.NET 7 introduces a new feature - generic math (read more here and here) which is based on addition of static abstract interface methods. This feature introduces a lot of interfaces which allow to generically abstract over number types and/or math operations:

class Fraction<T> :
    IAdditionOperators<Fraction<T>, Fraction<T>, Fraction<T>>,
    ISubtractionOperators<Fraction<T>, Fraction<T>, Fraction<T>>,
    IDivisionOperators<Fraction<T>, Fraction<T>, Fraction<T>>
    where T : INumber<T>
{
    public T Numerator { get; }
    public T Denominator { get; }

    public Fraction(T numerator, T denominator)
    {
        Numerator = numerator;
        Denominator = denominator;
    }

    public static Fraction<T> operator +(Fraction<T> left, Fraction<T> right) =>
        new(left.Numerator * right.Denominator + right.Numerator * left.Denominator,
            left.Denominator * right.Denominator);

    public static Fraction<T> operator -(Fraction<T> left, Fraction<T> right) =>
        new(left.Numerator * right.Denominator - right.Numerator * left.Denominator,
            left.Denominator * right.Denominator);

    public static Fraction<T> operator /(Fraction<T> left, Fraction<T> right) =>
        new(left.Numerator * right.Denominator, left.Denominator * right.Numerator);
}

Comments

2

First, your class should limit the generic parameter to primitives ( public class Fraction where T : struct, new() ).

Second, you'll probably need to create implicit cast overloads so you can handle casting from one type to another without the compiler crying.

Third, you can overload the four basic operators as well to make the interface more flexible when combining fractions of different types.

Lastly, you have to consider how you are handling arithmetic over and underflows. A good library is going to be extremely explicit in how it handles overflows; otherwise you cannot trust the outcome of operations of different fraction types.

2 Comments

The problem is that I cannot even do sums like that because structs don't have an addition operator defined.
msdn.microsoft.com/en-us/library/aa691324(VS.71).aspx "user-defined implementations can be introduced by including operator declarations in classes and structs"
1

The other approaches here will work, but they have a high performance impact over raw operators. I figured I would post this here for someone who needs the fastest, not the prettiest approach.

If you want to do generic math without paying a performance penalty, then this is, unfortunately, the way to do it:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T IncrementToMax(T value)
{
    if (typeof(T) == typeof(char))
        return (char)(object)value! < char.MaxValue ? (T)(object)(char)((char)(object)value + 1) : value;
 
    if (typeof(T) == typeof(byte))
        return (byte)(object)value! < byte.MaxValue ? (T)(object)(byte)((byte)(object)value + 1) : value;

    // ...rest of the types
}

It looks horrific, I know, but using this method will produce code that runs as fast as possible. The JIT will optimize out all the casts and conditional branches.

You can read the explanation and some additional important details here: http://www.singulink.com/codeindex/post/generic-math-at-raw-operator-speed

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.