www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - Why not allow elementwise operations on tuples?

reply Sergei Nosov <sergei.nosov gmail.com> writes:
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
parent reply "H. S. Teoh" <hsteoh qfbox.info> writes:
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
parent reply Sergei Nosov <sergei.nosov gmail.com> writes:
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:
 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.
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 16 2023
parent reply JG <someone simewhere.com> writes:
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:
 [...]
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?
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
Jan 18 2023
parent Sergei Nosov <sergei.nosov gmail.com> writes:
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) etc
That's where `areCompatibleTuples` function comes in!
Jan 19 2023