www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - opSlice() or opIndex() for the entire slice?

reply =?UTF-8?B?QWxpIMOHZWhyZWxp?= <acehreli yahoo.com> writes:
The documentation says "To overload a[], simply define opIndex with no 
parameters":

   http://dlang.org/operatoroverloading.html#Slice

And it works with some uses of a[]. However, opSlice() seems to be 
needed to actually use the returned slice further.

Note that opSlice() also seems to be for backward compatibility: "For 
backward compatibility, a[] and a[i..j] can also be overloaded by 
implementing opSlice() with no arguments and opSlice(i, j) with two 
arguments, respectively. This only applies for one-dimensional slicing, 
and dates from when D did not have full support for multidimensional 
arrays. This usage of opSlice is discouraged."

How can I achieve the last line in main() without needing opSlice()? I 
am trying to let the Slice type handle opAssign() and friends.

import std.stdio;

struct Collection
{
     int[] elements;

     /* Handles the slice operations */
     struct Slice
     {
         int[] slice;

         Slice opAssign(int value)
         {
             slice[] = value;
             return this;
         }
     }

     Slice opIndex()
     {
         writeln("opIndex");
         return Slice(elements);
     }

     Slice opSlice()
     {
         writeln("opSlice");
         return Slice(elements);
     }
}

void main()
{
     auto c = Collection([ 0, 1, 2, 3]);

     // This works with either operator but opIndex is favored. (You can
     // comment out opSlice() and it will still work.)
     writeln(c[]);

     // This requires opSlice.
     c[] = 42;
}

Ali
Sep 04 2014
next sibling parent reply "Marc =?UTF-8?B?U2Now7x0eiI=?= <schuetzm gmx.net> writes:
On Thursday, 4 September 2014 at 19:12:27 UTC, Ali Çehreli wrote:
 The documentation says "To overload a[], simply define opIndex 
 with no parameters":

   http://dlang.org/operatoroverloading.html#Slice

 And it works with some uses of a[]. However, opSlice() seems to 
 be needed to actually use the returned slice further.

 Note that opSlice() also seems to be for backward 
 compatibility: "For backward compatibility, a[] and a[i..j] can 
 also be overloaded by implementing opSlice() with no arguments 
 and opSlice(i, j) with two arguments, respectively. This only 
 applies for one-dimensional slicing, and dates from when D did 
 not have full support for multidimensional arrays. This usage 
 of opSlice is discouraged."

 How can I achieve the last line in main() without needing 
 opSlice()? I am trying to let the Slice type handle opAssign() 
 and friends.
--8<-- snip --8<--
     // This requires opSlice.
     c[] = 42;
 }
Have you tried opIndexAssign()?
Sep 04 2014
parent =?UTF-8?B?QWxpIMOHZWhyZWxp?= <acehreli yahoo.com> writes:
On 09/04/2014 12:19 PM, "Marc Schütz" <schuetzm gmx.net>" wrote:
 On Thursday, 4 September 2014 at 19:12:27 UTC, Ali Çehreli wrote:
 The documentation says "To overload a[], simply define opIndex with no
 parameters":

   http://dlang.org/operatoroverloading.html#Slice

 And it works with some uses of a[]. However, opSlice() seems to be
 needed to actually use the returned slice further.

 Note that opSlice() also seems to be for backward compatibility: "For
 backward compatibility, a[] and a[i..j] can also be overloaded by
 implementing opSlice() with no arguments and opSlice(i, j) with two
 arguments, respectively. This only applies for one-dimensional
 slicing, and dates from when D did not have full support for
 multidimensional arrays. This usage of opSlice is discouraged."

 How can I achieve the last line in main() without needing opSlice()? I
 am trying to let the Slice type handle opAssign() and friends.
--8<-- snip --8<--
     // This requires opSlice.
     c[] = 42;
 }
Have you tried opIndexAssign()?
That works and kind of makes sense. My main confusion stems from the fact that the new templated opSlice() does not have its own section on the documentation page, other than appearing in examples of other operators. What I did not like about opIndexAssign() at first is the fact that it puts the Collection in the middle and requires an explicit dispatch by the programmer to Slice. In contrast, the legacy way (which still works) bypasses Collection and goes directly to Slice. However, the legacy way supports only one dimension. Also, the new way is necessary as the same function (namely, opIndexAssign) takes care of both indexing and slicing. This is from the change log: void opIndexAssign(A...)(E val, A indices) { assert(A.length == payload.length); foreach (dim, x; indices) { static if (is(typeof(x) : size_t)) { // this[..., x, ...] payload[dim][x] = val; } else { // this[..., x[0] .. x[1], ...] payload[dim][x[0] .. x[1]] = val; } } } So, the following is my current solution. Note that opIndex() seems still be necessary. Is it possible to replace it with an opSlice overload? import std.stdio; struct Collection { int[] elements; /* Handles the slice operations */ struct Slice { int[] slice; Slice opAssign(int value) { writefln("Slice.opAssign(int)"); slice[] = value; return this; } } size_t opDollar() const { writefln("opDollar()"); return elements.length; } // CHALLENGE: Can you replace this with an opSlice overload? Slice opIndex() { writefln("opIndex()"); return Slice(elements); } Slice opSlice(size_t dim, A...)(A args) { writefln("opSlice!%s(%(%s, %))", dim, [ args ]); return Slice(elements[args[0] .. args[1]]); } Collection opIndexAssign(A...)(int value, A indexes) { writefln("opIndexAssign(%s, %(%s, %))", value, [ indexes ]); foreach (dim, x; indexes) { static if (is(typeof(x) : size_t)) { elements[x] = value; } else static if (is(typeof(x) : Slice)) { x = value; } else { static assert(false); } } return this; } } void main() { auto c = Collection([ 0, 1, 2, 3]); writeln("\n--- c[] ---"); c[]; writeln("\n--- c[1 .. $-1] = 42 ---"); c[1 .. $-1] = 42; writeln("\n--- c[3] = 100 ---"); c[3] = 100; assert(c.elements == [ 0, 42, 42, 100 ]); } The output: --- c[] --- opIndex() --- c[1 .. $-1] = 42 --- opDollar() opSlice!0(1, 3) opIndexAssign(42, Slice([1, 2])) Slice.opAssign(int) --- c[3] = 100 --- opIndexAssign(100, 3) Ali
Sep 04 2014
prev sibling next sibling parent reply "monarch_dodra" <monarchdodra gmail.com> writes:
On Thursday, 4 September 2014 at 19:12:27 UTC, Ali Çehreli wrote:
 The documentation says "To overload a[], simply define opIndex 
 with no parameters":

   http://dlang.org/operatoroverloading.html#Slice

 And it works with some uses of a[]. However, opSlice() seems to 
 be needed to actually use the returned slice further.
This must be new, as I've read that page several times, and have never seen that before. Furthermore, I've done a *lot* pulls to improve ranges' slicing capabilities, and no reviewer has ever mentioned this before.
Sep 04 2014
next sibling parent =?UTF-8?B?QWxpIMOHZWhyZWxp?= <acehreli yahoo.com> writes:
On 09/04/2014 02:04 PM, monarch_dodra wrote:

 On Thursday, 4 September 2014 at 19:12:27 UTC, Ali Çehreli wrote:
 The documentation says "To overload a[], simply define opIndex with no
 parameters":

   http://dlang.org/operatoroverloading.html#Slice

 And it works with some uses of a[]. However, opSlice() seems to be
 needed to actually use the returned slice further.
This must be new, as I've read that page several times, and have never seen that before. Furthermore, I've done a *lot* pulls to improve ranges' slicing capabilities, and no reviewer has ever mentioned this before.
Yeah, there is something fishy with these operators. Please see my other post. It seems like opIndex() is necessary or at least more convenient. Ali
Sep 04 2014
prev sibling parent reply "H. S. Teoh via Digitalmars-d-learn" <digitalmars-d-learn puremagic.com> writes:
On Thu, Sep 04, 2014 at 09:04:33PM +0000, monarch_dodra via Digitalmars-d-learn
wrote:
 On Thursday, 4 September 2014 at 19:12:27 UTC, Ali Çehreli wrote:
The documentation says "To overload a[], simply define opIndex with
no parameters":

  http://dlang.org/operatoroverloading.html#Slice

And it works with some uses of a[]. However, opSlice() seems to be
needed to actually use the returned slice further.
This must be new, as I've read that page several times, and have never seen that before. Furthermore, I've done a *lot* pulls to improve ranges' slicing capabilities, and no reviewer has ever mentioned this before.
I'm the one responsible for this. It's based on Kenji's PR that implements multidimensional slicing; in his design, he specifically relegates opSlice to returning index range proxy objects, so that mixed single-indexing and range-indexing (i.e., expressions of the form A[1, 2..3, 4, 5..6]) will be easily implementable. Under this design, x..y is translated into A.opSlice(x,y), and the actual indexing with [] is translated into A.opIndex(...), so in this case it becomes: A.opIndex(1, A.opSlice(2,3), 4, A.opSlice(5,6)); The idea, really, is that by this unification we can write a generic opIndex that handles any combination of single indices and index ranges: struct Array { ProxyObj opSlice(int i, int j) { ... } void opIndex(A...)(A args) { foreach (arg; args) { static if (is(typeof(arg) == ProxyObj)) { // implement index range here } else if (is(typeof(arg) : size_t)) { // implement single index lookup // here } } } } By extension, therefore, it follows that implementing slice operations of the form A[] simply involves invoking A.opIndex() with no arguments. Of course, to maintain backward compatibility, Kenji's PR left the old semantics of opSlice intact, if no suitable implementation of opIndex is found for expressions of the form A[]. So this is what I documented on that page. T -- Question authority. Don't ask why, just do it.
Sep 09 2014
parent =?UTF-8?B?QWxpIMOHZWhyZWxp?= <acehreli yahoo.com> writes:
On 09/09/2014 09:56 PM, H. S. Teoh via Digitalmars-d-learn wrote:

 Kenji's PR that
 implements multidimensional slicing; in his design, he specifically
 relegates opSlice to returning index range proxy objects
That's exactly how it finally made sense to me. :)
 By extension, therefore, it follows that implementing slice operations
 of the form A[] simply involves invoking A.opIndex() with no arguments.
I am currently writing an update to my Operator Overloading chapter to explain all of that. Ali
Sep 09 2014
prev sibling parent =?UTF-8?B?QWxpIMOHZWhyZWxp?= <acehreli yahoo.com> writes:
I think I figured this one out.

Before 2.066, we did not have proper support for multi-dimensional 
slicing. The following were the semantics we had:

-- The opIndex() overloads provided access to direct elements. Since 
multi-dimensional support was incomplete, opIndex() was about accessing 
a single object:

     a[i]     // element i by opIndex(size_t)
     a[i, j]  // element i,j by opIndex(size_t, size_t)

-- The two overloads of opSlice() returned range objects that 
represented either all of the elements or a slice of them:

     a[]        // opSlice()
     a[i..j]    // opSlice(size_t, size_t)

Note that those features are still usable but according to 
documentation, they are discouraged.

Since 2.066, we have multi-dimensional slicing. The way we look at these 
operators have changed a little:

-- The opIndex() overloads now return ranges that represent a range of 
elements. For example, if 'a' is a two-dimensional matrix, the first 
line below should return a sub-matrix inside it, not a single element:

     a[i..j, k..l]        // opIndex(MyRange, MyRange)

The following can indeed return a single element but I think it is a 
valid design decision to return a sub-matrix consisting of a single 
element as well:

     a[i, j]    // a matrix of one element by opIndex(size_t size_t)

A single matrix row:

     a[i, j..j]           // opIndex(size_t, MyRange)

Here is the answer to my original question: Consistent with the above, 
now opIndex() must take the responsibility of returning all of the elements:

     a[]                   // opIndex()

-- With that change of responsibility, what remains for opSlice() is the 
only task of producing a range object that opIndex() can later use to 
represent one or more elements:

     a[i..j, k..l]  // opIndex(opSlice(i, j), opSlice(k, l))

In summary, the following is what opSlice() should do almost in all cases:

     Tuple!(size_t size_t) opSlice(size_t beg, size_t end)
     {
         return tuple(beg, end);
     }

Also note that representing i..j and k..l can always be done by a 
Tuple!(size_t, size_t) without loss of any information. (i.e. MyRange 
above can actually be Tuple!(size_t, size_t)):

I am attaching an example that helped me understand what was going on. 
Note that the program is decidedly "elegant" as opposed to "efficient". 
:) For example, even initializing a single element by random access goes 
through these steps:

Initializing 1 of 80
deneme.Matrix.opIndexAssign!(int, int).opIndexAssign
deneme.Matrix.opIndex!(int, int).opIndex
deneme.Matrix.opDollar!0LU.opDollar
deneme.Matrix.opDollar!1LU.opDollar
deneme.Matrix.subMatrix
deneme.Matrix.this
deneme.Matrix.opAssign
[...]

But I like it. :)

Ali

import std.stdio;
import std.format;
import std.string;

struct Matrix
{
private:

     int[][] rows;

     /* Represents a range of rows of columns. */
     struct Range
     {
         size_t beg;
         size_t end;
     }

     /* Returns a reference to a sub-matrix that correspond to
      * the range arguments. */
     Matrix subMatrix(Range rowRange, Range columnRange)
     {
         writeln(__FUNCTION__);

         int[][] slices;

         foreach (row; rows[rowRange.beg .. rowRange.end]) {
             slices ~= row[columnRange.beg .. columnRange.end];
         }

         return Matrix(slices);
     }

public:

     this(size_t height, size_t width)
     {
         writeln(__FUNCTION__);

         rows = new int[][](height, width);
     }

     this(int[][] rows)
     {
         writeln(__FUNCTION__);

         this.rows = rows;
     }

     void toString(void delegate(const(char)[]) sink) const
     {
         formattedWrite(sink, "%(%(%5s %)\n%)", rows);
     }

     /* Assigns the value to all of the elements of the
      * matrix. */
     Matrix opAssign(int value)
     {
         writeln(__FUNCTION__);

         foreach (row; rows) {
             row[] = value;
         }

         return this;
     }

     /* Applies the operation to each element and assigns the
      * result back to it. e.g. 'm += 42'*/
     Matrix opOpAssign(string op)(int value)
     {
         writeln(__FUNCTION__);

         foreach (row; rows) {
             mixin ("row[] " ~ op ~ "= value;");
         }

         return this;
     }

     /* Returns the size of the provided dimension. */
     size_t opDollar(size_t dimension)() const
     {
         writeln(__FUNCTION__);

         static if (dimension == 0) {
             return rows.length;

         } else static if (dimension == 1) {
             return rows.length ? rows[0].length : 0;

         } else {
             static assert(false,
                           format("Invalid dimension: %s",
                                  dimension));
         }
     }

     /* Returns a range representing the provided indices. */
     Range opSlice(size_t dimension)(size_t beg, size_t end)
     {
         writeln(__FUNCTION__);

         return Range(beg, end);
     }

     /* Returns a sub-matrix corresponding to the arguments. */
     Matrix opIndex(A...)(A args)
     {
         writeln(__FUNCTION__);

         Range[2] ranges = [ Range(0, opDollar!0),
                             Range(0, opDollar!1) ];

         foreach (i, a; args) {
             static if (is (typeof(a) == Range)) {
                 ranges[i] = a;

             } else static if (is (typeof(a) : size_t)) {
                 ranges[i] = Range(a, a + 1);

             } else {
                 static assert(false,
                               format("Invalid index: %s",
                                      typeof(a).stringof));
             }
         }

         return subMatrix(ranges[0], ranges[1]);
     }

     /* Assigns the value to each element of the sub-matrix
      * determined by the arguments. */
     Matrix opIndexAssign(A...)(int value, A args)
     {
         writeln(__FUNCTION__);

         Matrix subMatrix = opIndex(args);
         return subMatrix = value;
     }

     /* Applies the operation to each element of the
      * sub-matrix. e.g. 'm[i, j] += 42'*/
     Matrix opIndexOpAssign(string op, A...)(int value, A args)
     {
         writeln(__FUNCTION__);

         Matrix subMatrix = opIndex(args);
         mixin ("return subMatrix " ~ op ~ "= value;");
     }
}

/* Applies the provided code and prints the result as well as
  * the new state of the matrix. */
void apply(string code)(Matrix m)
{
     writefln("\n--- %s ---", code);
     mixin ("auto result = (" ~ code ~ ");");
     writefln("result:\n%s", result);
     writefln("m:\n%s", m);
}

void main()
{
     enum height = 10;
     enum width = 8;

     auto m = Matrix(height, width);

     int counter = 0;
     foreach (column; 0 .. height) {
         foreach (row; 0 .. width) {
             writefln("Initializing %s of %s",
                      counter + 1, height * width);

             m[column, row] = counter;
             ++counter;
         }
     }

     writeln(m);

     apply!("m[1, 1] = 42")(m);
     apply!("m[0, 1 .. $] = 43")(m);
     apply!("m[0 .. $, 3] = 44")(m);
     apply!("m[$-4 .. $-1, $-4 .. $-1] = 7")(m);

     apply!("m[1, 1] *= 2")(m);
     apply!("m[0, 1 .. $] *= 4")(m);
     apply!("m[0 .. $, 0] *= 10")(m);
     apply!("m[$-4 .. $-2, $-4 .. $-2] -= 666")(m);

     apply!("m[1, 1]")(m);
     apply!("m[2, 0 .. $]")(m);
     apply!("m[0 .. $, 2]")(m);
     apply!("m[0 .. $ / 2, 0 .. $ / 2]")(m);

     apply!("++m[1..3, 1..3]")(m);
     apply!("--m[2..5, 2..5]")(m);

     apply!("m[]")(m);
     apply!("m[] = 20")(m);
     apply!("m[] /= 4")(m);
     apply!("(m[] += 5) /= 10")(m);
}
Sep 07 2014