It's dangerous to code alone! Take this.

Generic Math Part 3: Making an Interface with Operators

Published on 20 Oct 2022.

This is Part 4 in a series about generic math. You can view all parts here: Part 1 Part 2 Part 3 Part 4

This is the fourth post in a series about generic math. We’ve managed to solve our problem with a generic method and the IAdditionOperators<TSelf, TOther, TResult> interface, but now let’s take a peek at what else is available. This isn’t a deep dive into every single interface, but will give us an overview.

Operator-Based Interfaces

Like IAdditionOperators, there are quite a few interfaces that define a single operator or a small group of closely related operators. These are very useful and it is not hard to add them to your own types when adding operator overloads, if you have a desire to do so.

  • IAdditionOperators: Requires the + operator.
  • ISubtractionOperators: Requires the subtraction operator, -.
  • IMultiplyOperators: Requires the * operator.
  • IDivisionOperators: Requires the / operator.
  • IModulusOperators: Requires the remainder operator %.
  • IIncrementOperators: Requires the ++ operator.
  • IDecrementOperators: Requires the -- operator.
  • IUnaryNegationOperators: Requires the unary negation operator, -. (This is for things like -a, contrasted with a - b.)
  • IUnaryPlusOperators: Requires the unary plus operator, +. (This is for things like +a, contrasted with a + b.)
  • IEqualityOperators: Requires the == and != operators.
  • IComparisonOperators: Requires the comparison operators, >, <, <=, and >=.
  • IBitwiseOperators: Requires the bitwise operators, &, |, ^, and ~.
  • IShiftOperators: Requires the bit shifting operators, <<, >>, and >>>.

Static Method-Based Interfaces

There is a second pile of interfaces that don’t define operators, but, rather, define small collections of static methods.

An example of where these things could be useful is a method that needs to compute a square root. The Math class has a method for exactly this, public static double Sqrt(double value), but that only works if you know ahead of time that you have a double. Similarly, MathF has a method for the float type: public static float Sqrt(float value). But again, you need to know ahead of time that you have a float. While you often know exactly what you’ve got ahead of time, if you could define this in a generic way, it could make it easier to write code that needs to use Sqrt but wants to be generic to handle, say, both float and double.

Indeed, this second group of interfaces is more about simple things like allowing a single generic algorithm to work for float, double, and decimal, or a single algorithm for int and long instead of bringing in all sorts of crazy concepts like our Point class, over the last few posts in this series.

So here’s the interfaces from this set:

  • IRootFunctions: This interface defines static methods for roots (Sqrt for square roots, Cbrt for cube roots, RootN for the nth root, etc.).
  • IPowerFunctions: This interface defines the static Pow function.
  • IExponentialFunctions: This interface defines static methods for exponential operations like 10^x.
  • ILogarithmicFunctions: This interface defines static methods for logarithmic functions, such as Log.
  • ITrigonometricFunctions: This interface defines static methods for the trigonometric functions like sine, cosine, tangent, and their inverses.
  • IHyperbolicFunctions: This interface defines static functions for the hyperbolic functions like Sinh, Cosh, and Tanh.
  • IFloatingPointConstants: This interface defines static, generic constants for the mathematical numbers E, Pi, and Tau.
  • IMinMaxValue: This interface defines static properties for MinValue and MaxValue.
  • IAdditiveIdentity: This interface defines a static property for the type’s additive identity–a value that when added to other things leaves it unchanged. This is an analog to the number zero.
  • IMultiplicativeIdentity: This interface defines a static property for the type’s multiplicative identity–a value that when multiplied with other things leaves the thing unchanged. This is analog to the number one.

You probably won’t implement these interfaces yourself, but the different built-in types support these as makes sense. On the other hand, if you were building out something that was truly a representation of a number, such as a Fraction type that represents fractional values like 3/4 without turning them into a floating-point number, you may very well find yourself implementing all of these.

The Aggregation Interfaces

The previous two bundles of interfaces are very granular. Everything is as small as it can reasonably be and still make sense. But the interfaces that follow define much larger concepts. These interfaces extend many of the interfaces above, and some add additional required static members as well.

  • INumberBase<T> is the simplest, but it is not simple at all. It represents a foundational interface that covers most numeric operations, inluding +, -, *, /, ==, !=, ++, and --.
  • INumber<T> represents an arbitrary number of any type, building upon INumberBase<T>, and adds the comparison operators, <, >, <=, >=.
  • IUnsignedNumber<T> represents unsigned numbers. It augments INumberBase<T> but not INumber<T>. This is a useful type if you want to restrict things to definitely being unsigned and incapable of representing negative numbers.
  • ISignedNumber<T> represents signed numbers. It augments INumberBase<T> but not INumber<T>. This is useful if you want to ensure something is a signed number and can definitely represent at least some negative numbers (depending on its range).
  • IFloatingPoint<T>. This represents a floating-point number, so using it will guarantee that you have somethign that can represent non-integers.
  • IBinaryNumber<T>. This represents a number with a binary representation. This is a bit misleading, because there really isn’t a way to represent anything on the computer without it being binary. But this is what you might use if you want to work with the individual bits that compose a number. This builds on INumber<T>, but also adds in the shift and bitwise operators for bit manipulation.
  • IBinaryInteger<T>. This represents a number that can be manipulated at the bit level but that is also known to be an integer, incapable of storing fractional/decimal values.
  • IFloatingPointIeee754<T>. This awkward name is an interface that augments IFloatingPoint<T> but that makes promises that it works in accordance with the IEEE (“eye triple ee”) 754 standard, which includes things like a concept of NaN and infinity.
  • IBinaryFloatingPointIeee754. This is an IEEE 754-compliant floating point number with a binary representation. float and double both implement this.

The sheer number of members that you need to add to implement even the humble INumberBase<T> is quite a bit. It likely isn’t worth going to that trouble unless you’re building a key numeric type that you want to use widely, and especially one you want to be able to swap in different algorithms for the other numeric types like float or int. But if you are doing that, you can climb as far up this interface tree as you need.

Fhew! That was a lot. And we didn’t cover all the details. You can go explore these interfaces in the documentation on learn.microsoft.com.

This is Part 4 in a series about generic math. You can view all parts here: Part 1 Part 2 Part 3 Part 4