Implement type constraints like in C #

7 minute read

In the development of C # port of AtCoderLibrary Class <arbitrary numeric type I have the intention of realizing a type like> , and I will describe the result of the examination.

As an example for explanation, consider the following program that calculates the greatest common divisor.

class Program
{
    static void Main()
    {
        System.Console.WriteLine(new Gcder(30030).Calc(42)); // → 42
        System.Console.WriteLine(new Gcder(120).Calc(119)); // → 1
    }
}
class Gcder
{
    static int Gcd(int a, int b) => b > a ? Gcd(b, a) : (b == 0 ? a : Gcd(b, a % b));
    public int a;
    public Gcder(int a) { this.a = a; }
    public int Calc(int b) => Gcd(a, b);
}

Problem: Numerical calculation cannot be made Generic

If you try to make this generic, it will not be straightforward.

This is because b> a, b == 0, ʻa% b`, etc. are undefined.

class Program
{
    static void Main()
    {
        System.Console.WriteLine(new Gcder<int>(30030).Calc(42));
        System.Console.WriteLine(new Gcder<ulong>(30030).Calc(42));
    }
}
class Gcder<T> where T : struct
{
    static T Gcd(T a, T b) => b > a ? Gcd(b, a) : (b == 0 ? a : Gcd(b, a % b)); //This line cannot be compiled
    public T a;
    public Gcder(T a) { this.a = a; }
    public T Calc(T b) => Gcd(a, b);
}

Realization in C ++

In C ++, it can be expressed using the conditional expression of template.

#include <type_traits>
#include <iostream>

template <class T>
auto gcd(T a, T b) -> typename std::enable_if<std::is_integral<T>::value, T>::type
{
  return b > a ? gcd(b, a) : (b == 0 ? a : gcd(b, a % b));
}

int main()
{
  std::cout << gcd(120, 96) << std::endl;
  std::cout << gcd(120ULL, 96ULL) << std::endl;
  // std::cout << gcd("120", 96) << std::endl; // Compilation error
}

Solution: Interface for numerical calculation

interface definition

It is good to prepare an interface for numerical calculation.

public interface INumOperator<T> : IEqualityComparer<T>, IComparer<T> where T : struct
{
        T MinValue { get; }
        T MaxValue { get; }
    T Add(T x, T y);
    T Subtract(T x, T y);
    T Multiply(T x, T y);
    T Divide(T x, T y);
    T Modulo(T x, T y);
    bool GreaterThan(T x, T y);
    bool GreaterThanOrEqual(T x, T y);
    bool LessThan(T x, T y);
    bool LessThanOrEqual(T x, T y);
}
public readonly struct IntOperator : INumOperator<int>
{
    public int MinValue => int.MinValue;
    public int MaxValue => int.MaxValue;
    public int Add(int x, int y) => x + y;
    public int Subtract(int x, int y) => x - y;
    public int Multiply(int x, int y) => x * y;
    public int Divide(int x, int y) => x / y;
    public int Modulo(int x, int y) => x % y;
    public bool GreaterThan(int x, int y) => x > y;
    public bool GreaterThanOrEqual(int x, int y) => x >= y;
    public bool LessThan(int x, int y) => x < y;
    public bool LessThanOrEqual(int x, int y) => x <= y;
    public int Compare(int x, int y) => x.CompareTo(y);
    public bool Equals(int x, int y) => x == y;
    public int GetHashCode(int obj) => obj.GetHashCode();
}
// LongOperator, UIntOperator,ULongOperator etc. are defined in the same way

The point here is that ʻIntOperator is struct`. Details will be described later.

Completed form

class Program
{
    static void Main()
    {
        System.Console.WriteLine(new Gcder<int, IntOperator>(30030).Calc(42));
        System.Console.WriteLine(new Gcder<ulong, ULongOperator>(30030).Calc(42));
        System.Console.WriteLine(new Gcder<int, IntOperator>(120).Calc(119));
        System.Console.WriteLine(new Gcder<ulong, ULongOperator>(120).Calc(119));
    }
}
class Gcder<T, TOp> where T : struct where TOp : struct, INumOperator<T>
{
    static readonly TOp op;
    static T Gcd(T a, T b) => op.GreaterThan(b, a) ? Gcd(b, a) : (op.Equals(b, default) ? a : Gcd(b, op.Modulo(a, b)));
    public T a;
    public Gcder(T a) { this.a = a; }
    public T Calc(T b) => Gcd(a, b);
}

Now, ʻint is also genericized for ʻulong.

Consideration

Reasons to implement a math interface in a struct

There are two main reasons to implement a math interface in a struct.

–struct is non-nullable
–When a struct enters a type argument, the type is expanded and fast

The former is easy because you can just use static readonly TOp op; without initializing it.

The latter is important.

If the arithmetic type is class, calls like ʻop.GreaterThan (b, a) are virtualized and will never be JIT optimized. On the other hand, in the case of struct, ʻop.GreaterThan (b, a) can be expanded by JIT optimization. Therefore, it will operate at high speed.

Benchmarks

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.1082 (1909/November2018Update/19H2)
Intel Core i7-4790 CPU 3.60GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.401
[Host] : .NET Core 3.1.7 (CoreCLR 4.700.20.36602, CoreFX 4.700.20.37001), X64 RyuJIT
DefaultJob : .NET Core 3.1.7 (CoreCLR 4.700.20.36602, CoreFX 4.700.20.37001), X64 RyuJIT

Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
DirectInt 1.173 s 0.0046 s 0.0038 s - - - 304 B
GenericInt 1.292 s 0.0054 s 0.0048 s - - - 2112 B
GenericClassInt 2.361 s 0.0463 s 0.0747 s - - - 304 B

Code</summary><div>

class Program
{
    static void Main()
    {
        BenchmarkRunner.Run<AclBench>(DefaultConfig.Instance.AddDiagnoser(MemoryDiagnoser.Default));
    }
}
class GcderInt
{
    static int Gcd(int a, int b) => b > a ? Gcd(b, a) : b == default ? a : Gcd(b, a % b);
    public int a;
    public GcderInt(int a) { this.a = a; }
    public int Calc(int b) => Gcd(a, b);
}
class Gcder<T, TOp> where T : struct where TOp : INumOperator<T>, new()
{
    static readonly TOp op = new TOp();
    static T Gcd(T a, T b) => op.GreaterThan(b, a) ? Gcd(b, a) : (op.Equals(b, default) ? a : Gcd(b, op.Modulo(a, b)));
    public T a;
    public Gcder(T a) { this.a = a; }
    public T Calc(T b) => Gcd(a, b);
}
public class ClassIntOperator : INumOperator<int>
{
    public int MinValue => int.MinValue;
    public int MaxValue => int.MaxValue;
    public int Add(int x, int y) => x + y;
    public int Subtract(int x, int y) => x - y;
    public int Multiply(int x, int y) => x * y;
    public int Divide(int x, int y) => x / y;
    public int Modulo(int x, int y) => x % y;
    public bool GreaterThan(int x, int y) => x > y;
    public bool GreaterThanOrEqual(int x, int y) => x >= y;
    public bool LessThan(int x, int y) => x < y;
    public bool LessThanOrEqual(int x, int y) => x <= y;
    public int Compare(int x, int y) => x.CompareTo(y);
    public bool Equals(int x, int y) => x == y;
    public int GetHashCode(int obj) => obj.GetHashCode();
}
public class AclBench
{
    const int N = 10_000_000;

    [Benchmark]
    public long DirectInt()
    {
        var rnd = new System.Random(42);
        long sum = 0;
        var gcder = new GcderInt(2 * 3 * 5 * 7 * 9 * 11 * 13 * 17 * 19 * 23);
        for (int i = 0; i < N; i++)
        {
            sum += gcder.Calc(rnd.Next());
        }
        return sum;
    }

    [Benchmark]
    public long GenericInt()
    {
        var rnd = new System.Random(42);
        long sum = 0;
        var gcder = new Gcder<int, IntOperator>(2 * 3 * 5 * 7 * 9 * 11 * 13 * 17 * 19 * 23);
        for (int i = 0; i < N; i++)
        {
            sum += gcder.Calc(rnd.Next());
        }
        return sum;
    }

    [Benchmark]
    public long GenericClassInt()
    {
        var rnd = new System.Random(42);
        long sum = 0;
        var gcder = new Gcder<int, ClassIntOperator>(2 * 3 * 5 * 7 * 9 * 11 * 13 * 17 * 19 * 23);
        for (int i = 0; i < N; i++)
        {
            sum += gcder.Calc(rnd.Next());
        }
        return sum;
    }
}
public interface INumOperator<T> : System.Collections.Generic.IEqualityComparer<T>, System.Collections.Generic.IComparer<T> where T : struct
{
    T MinValue { get; }
    T MaxValue { get; }
    T Add(T x, T y);
    T Subtract(T x, T y);
    T Multiply(T x, T y);
    T Divide(T x, T y);
    T Modulo(T x, T y);
    bool GreaterThan(T x, T y);
    bool GreaterThanOrEqual(T x, T y);
    bool LessThan(T x, T y);
    bool LessThanOrEqual(T x, T y);
}
public readonly struct IntOperator : INumOperator<int>
{
    public int MinValue => int.MinValue;
    public int MaxValue => int.MaxValue;
    public int Add(int x, int y) => x + y;
    public int Subtract(int x, int y) => x - y;
    public int Multiply(int x, int y) => x * y;
    public int Divide(int x, int y) => x / y;
    public int Modulo(int x, int y) => x % y;
    public bool GreaterThan(int x, int y) => x > y;
    public bool GreaterThanOrEqual(int x, int y) => x >= y;
    public bool LessThan(int x, int y) => x < y;
    public bool LessThanOrEqual(int x, int y) => x <= y;
    public int Compare(int x, int y) => x.CompareTo(y);
    public bool Equals(int x, int y) => x == y;
    public int GetHashCode(int obj) => obj.GetHashCode();
}

</div></details>

Digression

It seems that the function called Type Classes, which is targeted at C # 10.0, is exactly like this.

https://github.com/dotnet/csharplang/issues/110

Tags:

Updated: