Tuesday, April 23, 2013

Sasa.Operators<*> - Generic Arithmetic and Logical Operators

This is the fourteenth post in my ongoing series covering the abstractions in Sasa. Previous posts:

When writing generic code, it's pretty common to yearn for some generic arithmetic interface so you don't have to copy-paste the same function over and over to provide overloads for all the CLR's numeric types. Omitting a standard arithmetic interface was one of .NET's biggest mistakes in my opinion.

Enter Sasa.Operators<T>, Sasa.Operators<T0, T1>, and Sasa.Operators<T0, T1, T2>, which are static classes that expose delegates for all operators on any given types T, T0, T1, and T2. T must contain operator overloads for the corresponding operation to be available, or if T is a primitive type like Int32 or Double, then a delegate is dynamically generated and cached. If T does not contain operator overloads for a given operation, the corresponding delegate for that operation will be null, so you just need to check for their presence as a precondition.

Sasa.Operators<T>

Sasa.Operators<T> provides operators over a single generic type T. In other words, any operators defined only on a single type T will be available via this generic static class, eg. addition, subtraction, multiplication, etc.

Sasa.Operators<T>.Add

The standard addition operation, Sasa.Operators<T>.Add, is straightforward, taking two T arguments and returning their addition:

var x = Operators<int>.Add(1, 2);
Console.WriteLine(x);
// output:
// 3

Sasa.Operators<T>.And

The Sasa.Operators<T>.And operation takes two T arguments, and returns a T argument which is the bitwise/logical 'and' of the two arguments:

var x = Operators<int>.And(1, 3);
Console.WriteLine(x);
// output:
// 1

Sasa.Operators<T>.Decrement

The Sasa.Operators<T>.Decrement operation takes a T argument, and returns a "decremented" T argument:

var x = Operators<double>.Decrement(1.5);
Console.WriteLine(x);
// output:
// 0.5

Sasa.Operators<T>.Divide

The Sasa.Operators<T>.Divide operation two T arguments and returns a T argument corresponding to their division:

var x = Operators<int>.Divide(6, 2);
Console.WriteLine(x);
// output:
// 3

Sasa.Operators<T>.Equal

The Sasa.Operators<T>.Equal operation takes two T arguments and returns a bool argument corresponding to their equality:

var x = Operators<int>.Equal(6, 2);
Console.WriteLine(x);
// output:
// false

Sasa.Operators<T>.False

The Sasa.Operators<T>.False operation takes a T argument and returns a bool based on whether the argument corresponds to that T's false value:

Console.WriteLine(Operators<bool>.False(false));
Console.WriteLine(Operators<bool>.False(true));
// output:
// true
// false

Sasa.Operators<T>.GreaterThan

The Sasa.Operators<T>.GreaterThan operation takes two T arguments and returns a bool based on whether the first argument is greater than the second:

var x = Operators<int>.GreaterThan(1, 2);
Console.WriteLine(x);
// output:
// false

Sasa.Operators<T>.GreaterThanOrEqual

The Sasa.Operators<T>.GreaterThanOrEqual operation takes two T arguments and returns a bool based on whether the first argument is greater than or equal to the second:

var x = Operators<int>.GreaterThanOrEqual(1, 2);
Console.WriteLine(x);
// output:
// false

Sasa.Operators<T>.Increment

The Sasa.Operators<T>.Increment operation a T argument and returns an "incremented" T:

var x = Operators<int>.Increment(98);
Console.WriteLine(x);
// output:
// 99

Sasa.Operators<T>.LeftShift

The Sasa.Operators<T>.LeftShift operation a T argument, followed by an Int32 argument, and returns a T "left-shifted" by the Int32:

var x = Operators<int>.LeftShift(1, 3);
Console.WriteLine(x);
// output:
// 8

Sasa.Operators<T>.LessThan

The Sasa.Operators<T>.LessThan operation takes two T arguments and returns a bool based on whether the first argument is less than the second:

var x = Operators<int>.LessThan(1, 2);
Console.WriteLine(x);
// output:
// true

Sasa.Operators<T>.LessThanOrEqual

The Sasa.Operators<T>.GreaterThanOrEqual operation takes two T arguments and returns a bool based on whether the first argument is less than or equal to the second:

var x = Operators<int>.LessThanOrEqual(1, 2);
Console.WriteLine(x);
// output:
// true

Sasa.Operators<T>.Modulo

The Sasa.Operators<T>.Modulo operation takes two T arguments, and returns the modulus/remainder of dividing the two arguments:

var x = Operators<int>.Modulo(9, 2);
Console.WriteLine(x);
// output:
// 1

Sasa.Operators<T>.Multiply

The Sasa.Operators<T>.Multiply operation takes two T arguments, and returns their multiplication:

var x = Operators<int>.Multiply(9, 2);
Console.WriteLine(x);
// output:
// 18

Sasa.Operators<T>.Negate

The Sasa.Operators<T>.Negate operation takes a T argument, and returns its negation:

var x = Operators<int>.Negate(9);
Console.WriteLine(x);
// output:
// -9

Sasa.Operators<T>.Not

The Sasa.Operators<T>.Not operation takes a T argument, and returns logical/bitwise not of the value:

// given twos-complement, not(-1) == 0
var x = Operators<int>.Not(-1); 
Console.WriteLine(x);
// output:
// 0

Sasa.Operators<T>.NotEqual

The Sasa.Operators<T>.NotEqual operation takes two T arguments and returns a bool argument corresponding to their inequality:

var x = Operators<int>.NotEqual(6, 2);
Console.WriteLine(x);
// output:
// true

Sasa.Operators<T>.Or

The Sasa.Operators<T>.Or operation takes two T arguments, and returns their logical/bitwise-or:

var x = Operators<int>.Or(1, 2);
Console.WriteLine(x);
// output:
// 3

Sasa.Operators<T>.Plus

The Sasa.Operators<T>.Plus operation takes a T argument and returns their logical/bitwise-plus of the argument:

var x = Operators<int>.Plus(-1);
Console.WriteLine(x);
// output:
// -1

Sasa.Operators<T>.RightShift

The Sasa.Operators<T>.RightShift operation takes a T argument, followed by an Int32 argument, and returns a T "right-shifted" by the Int32:

var x = Operators<int>.RightShift(8, 3);
Console.WriteLine(x);
// output:
// 1

Sasa.Operators<T>.Subtract

The Sasa.Operators<T>.Subtract operation takes two T arguments, and returns their subtraction:

var x = Operators<int>.Subtract(1, 3);
Console.WriteLine(x);
// output:
// -2

Sasa.Operators<T>.True

The Sasa.Operators<T>.True operation takes a T argument and returns a bool based on whether the argument corresponds to that T's true value:

Console.WriteLine(Operators<bool>.True(true));
Console.WriteLine(Operators<bool>.True(false));
// output:
// true
// false

Sasa.Operators<T>.Xor

The Sasa.Operators<T>.Xor operation takes two T arguments, and returns their logical/bitwise exclusive-or:

var x = Operators<int>.Xor(1, 3);
Console.WriteLine(x);
// output:
// 2

Sasa.Operators<T0, T1>

Not all operators are defined over only a single type, some are defined over two types. Sasa.Operators<T0, T1> provides efficient access to such operators, like explicit or implicit casting from a type T0 to T1.

Sasa.Operators<T0, T1>.Decrement

The Sasa.Operators<T0, T1>.Decrement operation takes a T0 argument, and returns a "decremented" T1 argument:

var x = Operators<double, double>.Decrement(1.5);
Console.WriteLine(x);
// output:
// 0.5

Sasa.Operators<T0, T1>.Equal

The Sasa.Operators<T0, T1>.Equal operation takes a T0 and T1 and returns a bool argument corresponding to their equality:

var x = Operators<int, int>.Equal(6, 2);
Console.WriteLine(x);
// output:
// false

Sasa.Operators<T0, T1>.Explicit

The Sasa.Operators<T0, T1>.Explicit operation takes a T0 and returns a T1 corresponding to an explicit cast from T0 to T1:

var x = Operators<int, float>.Explicit(6);
Console.WriteLine(x);
// output:
// 6.0

Sasa.Operators<T0, T1>.GreaterThan

The Sasa.Operators<T0, T1>.GreaterThan operation takes a T0 and T1 and returns a bool based on whether the first argument is greater than the second:

var x = Operators<int, float>.GreaterThan(1, 2F);
Console.WriteLine(x);
// output:
// false

Sasa.Operators<T>.GreaterThanOrEqual

The Sasa.Operators<T0, T1>.GreaterThanOrEqual operation takes a T0 and T1 and returns a bool based on whether the first argument is greater than or equal to the second:

var x = Operators<int, float>.GreaterThanOrEqual(1, 2F);
Console.WriteLine(x);
// output:
// false

Sasa.Operators<T0, T1>.Implicit

The Sasa.Operators<T0, T1>.Implicit operation takes a T0 and returns a T1 corresponding to an implicit cast from T0 to T1:

var x = Operators<int, double>.Implicit(6);
Console.WriteLine(x);
// output:
// 6.0

Sasa.Operators<T0, T1>.Increment

The Sasa.Operators<T0, T1>.Increment operation a T0 argument and returns an "incremented" T1:

var x = Operators<int>.Increment(98);
Console.WriteLine(x);
// output:
// 99

Sasa.Operators<T0, T1>.LeftShift

The Sasa.Operators<T0, T1>.LeftShift operation a T0 argument, followed by an Int32 argument, and returns a T1 "left-shifted" by the Int32:

var x = Operators<int, int>.LeftShift(1, 3);
Console.WriteLine(x);
// output:
// 8

Sasa.Operators<T0, T1>.Negate

The Sasa.Operators<T0, T1>.Negate operation takes a T0 argument, and returns its negation as type T1:

var x = Operators<int, int>.Negate(9);
Console.WriteLine(x);
// output:
// -9

Sasa.Operators<T0, T1>.Not

The Sasa.Operators<T0, T1>.Not operation takes a T0 argument, and returns logical/bitwise not of the value as type T1:

// given twos-complement, not(-1) == 0
var x = Operators<int, int>.Not(-1); 
Console.WriteLine(x);
// output:
// 0

Sasa.Operators<T0, T1>.NotEqual

The Sasa.Operators<T0, T1>.NotEqual operation takes a T0 and T1 and returns a bool argument corresponding to their inequality:

var x = Operators<int, int>.NotEqual(6, 2);
Console.WriteLine(x);
// output:
// true

Sasa.Operators<T>.Or

The Sasa.Operators<T>.Or operation takes two T arguments, and returns their logical/bitwise-or:

var x = Operators<int>.Or(1, 2);
Console.WriteLine(x);
// output:
// 3

Sasa.Operators<T0, T1>.RightShift

The Sasa.Operators<T0, T1>.RightShift operation takes a T argument, followed by an Int32 argument, and returns a T "right-shifted" by the Int32:

var x = Operators<int>.RightShift(8, 3);
Console.WriteLine(x);
// output:
// 1

Sasa.Operators<T0, T1, T2>

Sasa.Operators<T0, T1, T2> is the granddaddy of the operators classes, as it exposes the full generality of binary operators over 3 possible type parameters. In other words, it's possible to define an addition operator that takes a T0 and a T1 and returns a T2. Unary operators can only have two possible type parameters, so they are only available via Sasa.Operators< T0, T1>.

Sasa.Operators<T0, T1, T2>.Add

The addition operator Sasa.Operators<T0, T1, T2>.Add, takes arguments T0 and T1 and returns their addition as type T2:

var x = Operators<int, long, double>.Add(1, 2);
Console.WriteLine(x);
// output:
// 3.0

Sasa.Operators<T0, T1, T2>.And

The Sasa.Operators<T0, T1, T2>.And takes arguments T0 and T1 and returns their bitwise/logical 'and' as type T2:

var x = Operators<int, uint, uint>.And(1, 3);
Console.WriteLine(x);
// output:
// 1

Sasa.Operators<T0, T1, T2>.Divide

The Sasa.Operators<T0, T1, T2>.Divide operation takes arguments T0 and T1 and returns their division as type T2:

var x = Operators<long, int, int>.Divide(6, 2);
Console.WriteLine(x);
// output:
// 3

Sasa.Operators<T0, T1, T2>.Equal

The Sasa.Operators<T0, T1, T2>.Equal operation takes arguments T0 and T1 and returns their equality as type T2, where T2 must implement the true/false operators:

var x = Operators<int, int, bool>.Equal(6, 2);
Console.WriteLine(x);
// output:
// false

Sasa.Operators<T0, T1, T2>.GreaterThan

The Sasa.Operators<T0, T1, T2>.GreaterThan operation takes arguments T0 and T1 and returns their > as type T2, where T2 must implement the true/false operators:

var x = Operators<int, int, bool>.GreaterThan(1, 2);
Console.WriteLine(x);
// output:
// false

Sasa.Operators<T0, T1, T2>.GreaterThanOrEqual

The Sasa.Operators<T0, T1, T2>.GreaterThanOrEqual operation takes arguments T0 and T1 and returns their >= as type T2, where T2 must implement the true/false operators:

var x = Operators<int, int, bool>.GreaterThanOrEqual(1, 2);
Console.WriteLine(x);
// output:
// false

Sasa.Operators<T0, T1, T2>.LessThan

The Sasa.Operators<T0, T1, T2>.LessThan operation takes arguments T0 and T1 and returns their < as type T2, where T2 must implement the true/false operators:

var x = Operators<int, int, bool>.LessThan(1, 2);
Console.WriteLine(x);
// output:
// true

Sasa.Operators<T0, T1, T2>.LessThanOrEqual

The Sasa.Operators<T0, T1, T2>.GreaterThanOrEqual operation takes arguments T0 and T1 and returns their <= as type T2, where T2 must implement the true/false operators:

var x = Operators<int, int, bool>.LessThanOrEqual(1, 2);
Console.WriteLine(x);
// output:
// true

Sasa.Operators<T0, T1, T2>.Modulo

The Sasa.Operators<T0, T1, T2>.Modulo operation takes arguments T0 and T1 and returns their modulus as type T2:

var x = Operators<int, int, int>.Modulo(9, 2);
Console.WriteLine(x);
// output:
// 1

Sasa.Operators<T0, T1, T2>.Multiply

The Sasa.Operators<T0, T1, T2>.Multiply operation takes arguments T0 and T1 and returns their multiplication as type T2:

var x = Operators<int, int, int>.Multiply(9, 2);
Console.WriteLine(x);
// output:
// 18

Sasa.Operators<T0, T1, T2>.NotEqual

The Sasa.Operators<T0, T1, T2>.NotEqual operation takes arguments T0 and T1 and returns their inequality as type T2, where T2 must implement the true/false operators:

var x = Operators<int, int, bool>.NotEqual(6, 2);
Console.WriteLine(x);
// output:
// true

Sasa.Operators<T0, T1, T2>.Or

The Sasa.Operators<T0, T1, T2>.Or operation takes arguments T0 and T1 and returns their logical/bitwise-or as type T2:

var x = Operators<int, uint, uint>.Or(1, 2);
Console.WriteLine(x);
// output:
// 3

Sasa.Operators<T0, T1, T2>.Subtract

The Sasa.Operators<T0, T1, T2>.Subtract operation takes arguments T0 and T1 and returns their subtraction as type T2:

var x = Operators<int, int, int>.Subtract(1, 3);
Console.WriteLine(x);
// output:
// -2

Sasa.Operators<T0, T1, T2>.Xor

The Sasa.Operators<T0, T1, T2>.Xor operation takes arguments T0 and T1 and returns their logical/bitwise exclusive-or as type T2:

var x = Operators<int, int, uint>.Xor(1, 3);
Console.WriteLine(x);
// output:
// 2

Most of the functions in Sasa that are defined on generic numeric types utilize these classes to avoid code duplication. For instance, Sasa.Numbers covered in a previous post, defines the UpTo extension method like so:

public static IEnumerable<T> UpTo<T>(this T start, T end, T step)
    where T : IComparable<T>
{
    if (start.CompareTo(end) > 0)
      throw new ArgumentOutOfRangeException("start", "Must be less than or equal to parameter 'end'.");
    if (step.CompareTo(default(T)) <= 0)
      throw new ArgumentOutOfRangeException("step", "Must be greater than 0.");
    if (Operators<T>.Add == null)
      throw new ArgumentException("T must provide an addition operator.");
    for (var i = start;
         i.CompareTo(end) < 0;
         i = Operators<T>.Add(i, step))
    {
        yield return i;
    }
}

3 comments:

Srdjan said...

Sandro, this is fantastic series of posts... It will become mandatory reading for my team. Thank you!

Sandro Magi said...

Glad you find it useful. Plenty more to come!

Sandro Magi said...

As detailed in a later post, operators have been significantly expanded, tested and optimized. I've updated this post to reflect the newly expanded API.