Thread overview
Why not allow elementwise operations on tuples?
Jan 13, 2023
Sergei Nosov
Jan 13, 2023
H. S. Teoh
Jan 16, 2023
Sergei Nosov
Jan 18, 2023
JG
Jan 19, 2023
Sergei Nosov
January 13, 2023

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?

January 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
January 16, 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:

>

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?

January 18, 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:

>

[...]

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

January 19, 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) etc

That's where areCompatibleTuples function comes in!