Proposal: NTuple into the STL and/or runtime? #113387
-
Note This proposal is inspired by the Julia Programming Language One usage of the NTuple is for N dimensional abstraction. I have noticed in the C# STL the usage of Vector2, Vector3 etc. While this does work, a lot of abstraction power can come from writing code that can take an arbitrary ntuple size (using generics for performance). Benefits:
Downsides:
I believe that the addition of the NTuples into the .NET ecosystem can increase the abstractional power of C# and allow for far more advanced numerical algorithms that would traditionally be very annoying to implement and not necessarily COMPLETE. Actual implementation into the STL could simply a library similar to below, but if the runtime implemented NTuples, the approach could be simplified (add in SIMD?). I was inspired to write the NTuple library to create a parallel to [GeometryBasics]. (https://github.com/JuliaGeometry/GeometryBasics.jl/tree/master) library Performance Note: This library produces a lot of garbage because of the closures at the moment... work will needed in order to make the speed (which it should be!) equivalent to manual versions. public struct Vec<T, NT> where NT: NTuple<T, NT> where T: IFloatingPointIeee754<T> {
public NT Data;
public Vec(T v) => Data = NT.Create(_ => v);
public Vec(NT data) => Data = data;
public Vec(Vec<T, NT> v) => Data = v.Data;
public static Vec<T, NT> Unit(int n) => new(NT.Create(i => i == n ? T.One : T.Zero));
public static Vec<T, NT> UnitX { get; } = Unit(0);
public static Vec<T, NT> UnitY { get; } = Unit(1);
public static Vec<T, NT> UnitZ { get; } = Unit(2);
public static Vec<T, NT> Zero { get; } = new(T.Zero);
[IgnoreDataMember] public T LengthSquared => Data.Reduce(T.One, (a, b) => a+b*b);
[IgnoreDataMember] public T Length => T.Sqrt(LengthSquared);
[IgnoreDataMember] public T ReciprocalSqrt => T.ReciprocalSqrtEstimate(LengthSquared);
[IgnoreDataMember] public T X => Data[0];
[IgnoreDataMember] public T Y => Data[1];
[IgnoreDataMember] public T Z => Data[2];
[IgnoreDataMember] public int Dimension => Data.Count;
public T this[int index] {
get => Data[index];
set => Data[index] = value;
}
public static Vec<T, NT> operator +(Vec<T, NT> a, Vec<T, NT> b) => new(NT.Create(i => a[i] + b[i]));
public static Vec<T, NT> operator -(Vec<T, NT> a, Vec<T, NT> b) => new(NT.Create(i => a[i] - b[i]));
public static Vec<T, NT> operator +(Vec<T, NT> a, T b) => new(NT.Create(i => a[i] + b));
public static Vec<T, NT> operator +(T a, Vec<T, NT> b) => new(NT.Create(i => a + b[i]));
public static Vec<T, NT> operator -(T a, Vec<T, NT> b) => new(NT.Create(i => a - b[i]));
public static Vec<T, NT> operator -(Vec<T, NT> a, T b) => new(NT.Create(i => a[i] - b));
public static Vec<T, NT> operator *(T a, Vec<T, NT> b) => new(NT.Create(i => a * b[i]));
public static Vec<T, NT> operator *(Vec<T, NT> a, T b) => new(NT.Create(i => a[i] * b));
public static Vec<T, NT> operator /(Vec<T, NT> a, T b) => new(NT.Create(i => a[i] / b));
}
//https://github.com/JuliaGeometry/GeometryBasics.jl/blob/master/src/primitives/rectangles.jl
public struct HyperRect<T, NT>(Vec<T, NT> origin, Vec<T, NT> widths) where T : IFloatingPointIeee754<T> where NT : NTuple<T, NT> {
public Vec<T, NT> Origin = origin, Widths = widths;
[IgnoreDataMember] public T Volume => Widths.Data.Prod();
[IgnoreDataMember] public Vec<T, NT> Minima => Origin;
[IgnoreDataMember] public Vec<T, NT> Maxima => Origin + Widths;
[IgnoreDataMember] public Vec<T, NT> Center => Origin + Widths/(T.One + T.One);
/*
* Check if Rect `this` is contained in `v`. This does not use
strict inequality, so Rects may share faces and this will still
return true.
*/
public bool Inside(HyperRect<T, NT> container) {
var maxThis = Maxima;
var maxV = container.Maxima;
for (var i = 0; i < Origin.Dimension; i++) {
if (!(maxThis[i] <= maxV[i] && Minima[i] >= container.Minima[i]))
return false;
}
return true;
}
}
The NTuple LibraryTo perform efficient field lookup using the element reference function I made a rule that the static allocated tuples should have a sequential layout with no packing, maybe this should be improved? Here is the base interface for the NTuple I made. It mostly just implements the list interface into all NTuple variants public interface NTuple<T, NT> : IList<T> where NT: NTuple<T, NT>{
public static abstract NT Create(Func<int, T> itr);
public static abstract ref T GetElementReference(ref NT v, int index);
public T Reduce(T identity, Func<T, T, T> reduction) {
var v = identity;
for (int i = 0; i < Count; i++)
v = reduction(v, this[i]);
return v;
}
IEnumerator<T> IEnumerable<T>.GetEnumerator() {
var i = 0;
while (i < Count)
yield return this[i];
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
bool ICollection<T>.IsReadOnly => false;
int IList<T>.IndexOf(T item) {
for (var i = 0; i < Count; i++) {
if (EqualityComparer<T>.Default.Equals(this[i], item))
return i;
}
return -1;
}
bool ICollection<T>.Contains(T item) {
foreach (var w in this) {
if (EqualityComparer<T>.Default.Equals(w, item))
return true;
}
return false;
}
void ICollection<T>.CopyTo(T[] array, int arrayIndex) => CopyTo(array.AsSpan(arrayIndex));
public void CopyTo(Span<T> values) {
for (int i = 0; i < Count; i++)
values[i] = this[i];
}
void ICollection<T>.Add(T item) => throw new NotSupportedException();
void ICollection<T>.Clear() => throw new NotSupportedException();
bool ICollection<T>.Remove(T item) => throw new NotSupportedException();
void IList<T>.Insert(int index, T item) => throw new NotSupportedException();
void IList<T>.RemoveAt(int index) => throw new NotSupportedException();
} Here are the first three ntuple's (I made up to seven) [StructLayout(LayoutKind.Explicit, Size = 0)]
public struct NTuple0<T> : NTuple<T, NTuple0<T>> {
[IgnoreDataMember] public int Count => 0;
public NTuple0(){}
public static NTuple0<T> Create(Func<int, T> itr) => default;
public static unsafe ref T GetElementReference(ref NTuple0<T> v, int index) => ref Unsafe.AsRef<T>((byte*)Unsafe.AsPointer(ref v));
public T Reduce(T identity, Func<T, T, T> reduction) => identity;
public T this[int index] {
get => throw new ArgumentOutOfRangeException(nameof(index));
set => throw new ArgumentOutOfRangeException(nameof(index));
}
public override string ToString() => "()";
}
[StructLayout(LayoutKind.Sequential, Pack = 0)]
public struct NTuple1<T> : NTuple<T, NTuple1<T>> {
[IgnoreDataMember] public int Count => 1;
public T Item1;
public NTuple1(){}
public NTuple1(T item1) => Item1 = item1;
public static NTuple1<T> Create(Func<int, T> itr) => new(itr(0));
public static unsafe ref T GetElementReference(ref NTuple1<T> v, int index) => ref Unsafe.AsRef<T>((byte*)Unsafe.AsPointer(ref v) + Unsafe.SizeOf<T>()*index);
public T Reduce(T identity, Func<T, T, T> reduction) => reduction(identity, Item1);
public T this[int index] {
get => index switch {
0 => Item1,
_ => throw new ArgumentOutOfRangeException(nameof(index))
};
set {
Item1 = index switch {
0 => value,
_ => throw new ArgumentOutOfRangeException(nameof(index))
};
}
}
public override string ToString() => $"({Item1})";
}
[StructLayout(LayoutKind.Sequential, Pack = 0)]
public struct NTuple2<T> : NTuple<T, NTuple2<T>> {
[IgnoreDataMember] public int Count => 2;
public T Item1, Item2;
public static NTuple2<T> Create(Func<int, T> itr) => new(itr(0), itr(1));
public static unsafe ref T GetElementReference(ref NTuple2<T> v, int index) => ref Unsafe.AsRef<T>((byte*)Unsafe.AsPointer(ref v) + Unsafe.SizeOf<T>()*index);
public T Reduce(T identity, Func<T, T, T> reduction) => reduction(reduction(identity, Item1), Item2);
public NTuple2(){}
public NTuple2(T item1, T item2) {
Item1 = item1;
Item2 = item2;
}
public T this[int index] {
get => index switch {
0 => Item1,
1 => Item2,
_ => throw new ArgumentOutOfRangeException(nameof(index))
};
set {
switch(index) {
case 0:
Item1 = value;
break;
case 1:
Item2 = value;
break;
default:
throw new ArgumentOutOfRangeException(nameof(index));
};
}
}
public override string ToString() => $"({Item1},{Item2})";
}
[StructLayout(LayoutKind.Sequential, Pack = 0)]
public struct NTuple3<T> : NTuple<T, NTuple3<T>> {
[IgnoreDataMember] public int Count => 3;
public T Item1, Item2, Item3;
public NTuple3(T item1, T item2, T item3) {
Item1 = item1;
Item2 = item2;
Item3 = item3;
}
public static NTuple3<T> Create(Func<int, T> itr) => new(itr(0), itr(1), itr(2));
public static unsafe ref T GetElementReference(ref NTuple3<T> v, int index) => ref Unsafe.AsRef<T>((byte*)Unsafe.AsPointer(ref v) + Unsafe.SizeOf<T>()*index);
public T Reduce(T identity, Func<T, T, T> reduction) => reduction(reduction(reduction(identity, Item1), Item2), Item3);
public T this[int index] {
get => index switch {
0 => Item1,
1 => Item2,
2 => Item3,
_ => throw new ArgumentOutOfRangeException(nameof(index))
};
set {
switch(index) {
case 0:
Item1 = value;
break;
case 1:
Item2 = value;
break;
case 2:
Item3 = value;
break;
default:
throw new ArgumentOutOfRangeException(nameof(index));
};
}
}
public override string ToString() => $"({Item1},{Item2},{Item3})";
} This breaks the memory layout of the static tuples but may still be useful for super high dimensional data (the classical approach to NTuples): public struct NTupleArray<T, N> : NTuple<T, NTupleArray<T, N>> where N: IValue<int> {
[IgnoreDataMember] public int Count => Items.Length;
public readonly T[] Items;
public NTupleArray(params T[] items) => Items = items;
public static NTupleArray<T, N> Create(Func<int, T> itr) {
var array = new T[N.Value];
for (var i = 0; i < N.Value; i++)
array[i] = itr(i);
return new(array);
}
public static ref T GetElementReference(ref NTupleArray<T, N> v, int index) => ref v.Items[index];
public T this[int index] {
get => Items[index];
set => Items[index] = value;
}
public override string ToString() => $"({string.Join(",", Items)})";
}
public interface IValue<T>{
public static abstract T Value { get; }
}
public interface IOneThousand : IValue<int> {
public static virtual int Value => 1000;
}
P.S You may have noticed I have been trying to do practices that make C# a little more julian (generics wise) ;). |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
Beta Was this translation helpful? Give feedback.
Indeed related to #111973 and #89730.