digitalmars.D.learn - Why not allow elementwise operations on tuples?
- Sergei Nosov (33/33) Jan 13 2023 Hey, everyone!
- H. S. Teoh (143/151) Jan 13 2023 I've written a Vec type that implements precisely this, using tuples
- Sergei Nosov (12/27) Jan 16 2023 Yeah, that's clear that such an implementation is rather
- JG (4/17) Jan 18 2023 I guess such a method wouldn't be particularly generic since a
- Sergei Nosov (2/5) Jan 19 2023 That's where `areCompatibleTuples` function comes in!
Hey, everyone! I was wondering if there's a strong reason behind not implementing elementwise operations on tuples? Say, I've decided to store 2d points in a `Tuple!(int, int)`. It would be convenient to just write `a + b` to yield another `Tuple!(int, int)`. I can resort to using `int []` arrays and write elementwise operations as `c[] = a[] + b[]` which is almost fine - but it uses dynamic allocation and forces the user to create an explicit destination variable. It seems a bit awkward given that it's fairly straightforward to write smth as ``` T opBinary(string op, T)(T lhs, T rhs) if (isTuple!T) { T result; static foreach (i; 0 .. T.Types.length) { mixin("result.field[i] = lhs.field[i]"~op~"rhs.field[i];"); } return result; } ``` You only need to turn it into a member function to make it work. You can even make it more general and allow such operations for different, but compatible tuple types (there's a function `areCompatibleTuples` to check for such compatibility). Yet, there's only a specialization for tuple concatenation of `opBinary` (and an implementation of `opCmp` and `opAssign`). So, to repeat the question - is this a deliberate decision to not implement the default elementwise operation?
Jan 13 2023
On Fri, Jan 13, 2023 at 02:22:34PM +0000, Sergei Nosov via Digitalmars-d-learn wrote:Hey, everyone! I was wondering if there's a strong reason behind not implementing elementwise operations on tuples? Say, I've decided to store 2d points in a `Tuple!(int, int)`. It would be convenient to just write `a + b` to yield another `Tuple!(int, int)`.I've written a Vec type that implements precisely this, using tuples behind the scenes as the implementation, and operator overloading to allow nice syntax for vector arithmetic. -----------------------------------snip------------------------------------ /** * Represents an n-dimensional vector of values. */ struct Vec(T, size_t n) { T[n] impl; alias impl this; /** * Per-element unary operations. */ Vec opUnary(string op)() if (is(typeof((T t) => mixin(op ~ "t")))) { Vec result; foreach (i, ref x; result.impl) x = mixin(op ~ "this[i]"); return result; } /** * Per-element binary operations. */ Vec opBinary(string op, U)(Vec!(U,n) v) if (is(typeof(mixin("T.init" ~ op ~ "U.init")))) { Vec result; foreach (i, ref x; result.impl) x = mixin("this[i]" ~ op ~ "v[i]"); return result; } /// ditto Vec opBinary(string op, U)(U y) if (isScalar!U && is(typeof(mixin("T.init" ~ op ~ "U.init")))) { Vec result; foreach (i, ref x; result.impl) x = mixin("this[i]" ~ op ~ "y"); return result; } /// ditto Vec opBinaryRight(string op, U)(U y) if (isScalar!U && is(typeof(mixin("U.init" ~ op ~ "T.init")))) { Vec result; foreach (i, ref x; result.impl) x = mixin("y" ~ op ~ "this[i]"); return result; } /** * Per-element assignment operators. */ void opOpAssign(string op, U)(Vec!(U,n) v) if (is(typeof({ T t; mixin("t " ~ op ~ "= U.init;"); }))) { foreach (i, ref x; impl) mixin("x " ~ op ~ "= v[i];"); } void toString(W)(W sink) const if (isOutputRange!(W, char)) { import std.format : formattedWrite; formattedWrite(sink, "(%-(%s,%))", impl[]); } } /** * Convenience function for creating vectors. * Returns: Vec!(U,n) instance where n = args.length, and U is the common type * of the elements given in args. A compile-time error results if the arguments * have no common type. */ auto vec(T...)(T args) { static if (args.length == 1 && is(T[0] == U[n], U, size_t n)) return Vec!(U, n)(args); else static if (is(typeof([args]) : U[], U)) return Vec!(U, args.length)([ args ]); else static assert(false, "No common type for " ~ T.stringof); } /// unittest { // Basic vector construction auto v1 = vec(1,2,3); static assert(is(typeof(v1) == Vec!(int,3))); assert(v1[0] == 1 && v1[1] == 2 && v1[2] == 3); // Vector comparison auto v2 = vec(1,2,3); assert(v1 == v2); // Unary operations assert(-v1 == vec(-1, -2, -3)); assert(++v2 == vec(2,3,4)); assert(v2 == vec(2,3,4)); assert(v2-- == vec(2,3,4)); assert(v2 == vec(1,2,3)); // Binary vector operations auto v3 = vec(2,3,1); assert(v1 + v3 == vec(3,5,4)); auto v4 = vec(1.1, 2.2, 3.3); static assert(is(typeof(v4) == Vec!(double,3))); assert(v4 + v1 == vec(2.1, 4.2, 6.3)); // Binary operations with scalars assert(vec(1,2,3)*2 == vec(2,4,6)); assert(vec(4,2,6)/2 == vec(2,1,3)); assert(3*vec(1,2,3) == vec(3,6,9)); // Non-numeric vectors auto sv1 = vec("a", "b"); static assert(is(typeof(sv1) == Vec!(string,2))); assert(sv1 ~ vec("c", "d") == vec("ac", "bd")); assert(sv1 ~ "post" == vec("apost", "bpost")); assert("pre" ~ sv1 == vec("prea", "preb")); } unittest { // Test opOpAssign. auto v = vec(1,2,3); auto w = vec(4,5,6); v += w; assert(v == vec(5,7,9)); } unittest { int[4] z = [ 1, 2, 3, 4 ]; auto v = vec(z); static assert(is(typeof(v) == Vec!(int,4))); assert(v == vec(1, 2, 3, 4)); } unittest { import std.format : format; auto v = vec(1,2,3,4); assert(format("%s", v) == "(1,2,3,4)"); } -----------------------------------snip------------------------------------ T -- Never ascribe to malice that which is adequately explained by incompetence. -- Napoleon Bonaparte
Jan 13 2023
On Friday, 13 January 2023 at 15:27:26 UTC, H. S. Teoh wrote:On Fri, Jan 13, 2023 at 02:22:34PM +0000, Sergei Nosov via Digitalmars-d-learn wrote:Yeah, that's clear that such an implementation is rather straightforward. Although, I'm a bit confused with your implementation - 1. it doesn't seem to use tuples behind the scenes despite your claim (it uses static array) 2. `alias impl this;` introduces some unexpected interactions (e.g. `~` and `toString` are "intercepted" by the array implementation and yield "wrong" results). Anyway, my original question was primarily about reasoning - why there's no implementation specifically for `std.Tuple`? If it's a "feature, not a bug" - what's the best way to provide an implementation on the client side?Hey, everyone! I was wondering if there's a strong reason behind not implementing elementwise operations on tuples? Say, I've decided to store 2d points in a `Tuple!(int, int)`. It would be convenient to just write `a + b` to yield another `Tuple!(int, int)`.I've written a Vec type that implements precisely this, using tuples behind the scenes as the implementation, and operator overloading to allow nice syntax for vector arithmetic.
Jan 16 2023
On Monday, 16 January 2023 at 08:30:15 UTC, Sergei Nosov wrote:On Friday, 13 January 2023 at 15:27:26 UTC, H. S. Teoh wrote:I guess such a method wouldn't be particularly generic since a tuple does not need to consist of types that have the same operations e.g. Tuple!(int,string) etc[...]Yeah, that's clear that such an implementation is rather straightforward. Although, I'm a bit confused with your implementation - 1. it doesn't seem to use tuples behind the scenes despite your claim (it uses static array) 2. `alias impl this;` introduces some unexpected interactions (e.g. `~` and `toString` are "intercepted" by the array implementation and yield "wrong" results). Anyway, my original question was primarily about reasoning - why there's no implementation specifically for `std.Tuple`? If it's a "feature, not a bug" - what's the best way to provide an implementation on the client side?
Jan 18 2023
On Wednesday, 18 January 2023 at 16:42:00 UTC, JG wrote:I guess such a method wouldn't be particularly generic since a tuple does not need to consist of types that have the same operations e.g. Tuple!(int,string) etcThat's where `areCompatibleTuples` function comes in!
Jan 19 2023