www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - Template class with dispatched properties

reply "Ross Hays" <throwaway email.net> writes:
I have been playing around with a vector implementation that I am
trying to write in D, and have been having problems getting
something to work (it probably isn't even possible).

My end goal is to be able to instantiate a vector with a syntax
like...
`Vector!(2, float) vec = new Vector!(2, float)();`

Now this part I can get working by having `class Vector(int i,
T)` and an array such as `T[i] data`. The thing that I have been
trying to do, is allow access to the components of the vector
through properties that are identified by letter. Ideally it
would work like `vec.x = 4.0f;` or something like that. I have
tried using  property and the opDispatch method together, but
that does not compile. I know I can just use array indicies to
access components, but first I wanted to see if this was possible.

Any suggestions?

Thank you,

Ross Hays
Nov 07 2013
parent reply "Chris Cain" <clcain uncg.edu> writes:
On Friday, 8 November 2013 at 02:13:01 UTC, Ross Hays wrote:
 My end goal is to be able to instantiate a vector with a syntax
 like...
 `Vector!(2, float) vec = new Vector!(2, float)();`

 ...

 Any suggestions?
Greetings, This works: --- import std.stdio; struct Vector(int N, T) if (N <= 3) { private T[N] data; public property void opDispatch(string fieldName, Args ...)(Args args) if (Args.length == 1 && fieldName.length == 1 && cast(size_t)(fieldName[0] - 'x') < N) { static immutable offset = cast(size_t)(fieldName[0] - 'x'); data[offset] = args[0]; } public property T opDispatch(string fieldName, Args ...)(Args args) if (Args.length == 0 && fieldName.length == 1 && cast(size_t)(fieldName[0] - 'x') < N) { static immutable offset = cast(size_t)(fieldName[0] - 'x'); return data[offset]; } } void main() { auto vec = Vector!(2, float)(); vec.x = 1.0; vec.y = 2.0; //vec.z = 3.0; // Doesn't compile, as expected for a Vector!(2, float); writeln("vec.x = ", vec.x, " and vec.y = ", vec.y); } --- Minor tweaks might be necessary, but that should get you started.
Nov 07 2013
parent reply "Chris Cain" <clcain uncg.edu> writes:
On Friday, 8 November 2013 at 02:48:31 UTC, Chris Cain wrote:
 Minor tweaks might be necessary, but that should get you 
 started.
Actually, I refactored it a little bit to make it better (original code was just a bit too messy for my taste): --- struct Vector(int N, T) if (N <= 3) { private T[N] data; private static size_t toOffset(string fieldName) { return cast(size_t)(fieldName[0] - 'x'); } public property auto opDispatch(string fieldName, Args ...)(Args args) if (Args.length < 2 && fieldName.length == 1 && toOffset(fieldName) < N) { static immutable offset = toOffset(fieldName); static if(Args.length == 0) { // getter return data[offset]; } else { // setter data[offset] = args[0]; } } } ---
Nov 07 2013
parent reply "Ross Hays" <throwaway email.net> writes:
Awesome that seems to do what I was going for. I had tried a 
similar approach with  property dispatch and the subtraction of 
'x', but I had left out the static if and had the opDispatch 
returning a ref of the entry in the array (so there would just be 
the one  property still) but that resulted in "Error: no property 
'x' for type 'test.Vector!(2, float).Vector'".

This is interesting, though probably not a very safe way to 
handle vectors in the real world (even more so if they are going 
to be vectors of more than length 3).
Nov 07 2013
next sibling parent reply "Ross Hays" <throwaway email.net> writes:
I am actually a little curious why my original approach did not 
work at all. Using some of what you provided and some of what I 
had I get the following:

import std.stdio;
import std.string;

class Vector(int N, T) if (N <= 3) {
     T[N] data;

     this()
     {
     	data[] = 0;
     }

      property ref T opDispatch(string fieldName, Args ...)(Args 
args)
         if (Args.length < 2 && fieldName.length == 1 && 
toOffset(fieldName) < N)
     {
	    int offset = fieldName.charAt(0) - 'x';
	    return data[offset] = args[0];
     }
}

void main()
{
	Vector!(2, float) t = new Vector!(2,float)();
	writeln(t.x);
}

Which still errors out with: Error: no property 'x' for type 
'test.Vector!(2, float).Vector'

So that is odd.
Nov 07 2013
parent reply "Chris Cain" <clcain uncg.edu> writes:
On Friday, 8 November 2013 at 03:42:12 UTC, Ross Hays wrote:
 I am actually a little curious why my original approach did not 
 work at all. Using some of what you provided and some of what I 
 had I get the following:

 import std.stdio;
 import std.string;

 class Vector(int N, T) if (N <= 3) {
     T[N] data;

     this()
     {
     	data[] = 0;
     }

      property ref T opDispatch(string fieldName, Args ...)(Args 
 args)
         if (Args.length < 2 && fieldName.length == 1 && 
 toOffset(fieldName) < N)
     {
 	    int offset = fieldName.charAt(0) - 'x';
 	    return data[offset] = args[0];
     }
 }

 void main()
 {
 	Vector!(2, float) t = new Vector!(2,float)();
 	writeln(t.x);
 }

 Which still errors out with: Error: no property 'x' for type 
 'test.Vector!(2, float).Vector'

 So that is odd.
Strange. I'm getting a different error, but I'm still running 2.063.2. The error I get is `Error: cannot resolve type for t.opDispatch!("x")` What version are you running? In any case, the reason apparently is multifold: 1. Apparently the proper error message isn't shown when using the property notation. (I'd have to check to see if it happens in 2.064 ... might be a fixed bug) 2. `.charAt(0)` doesn't exist for D's strings. You can just use bracket notation to access the index. 3. When args is empty (as it will be for a getter, when you call) args[0] doesn't exist, so `Error: array index [0] is outside array bounds [0 .. 0]` So fix 2 and 3 and it works for getting x. The reason I use a static if is to separate the cases where args has items and when it does not (since args[0] is invalid when args.length == 0), so that'll be necessary to get it to work.
Nov 07 2013
next sibling parent "Chris Cain" <clcain uncg.edu> writes:
On Friday, 8 November 2013 at 04:06:22 UTC, Chris Cain wrote:
 So fix 2 and 3 and it works for getting x.
Also, define `toOffset` in the template constraint.
Nov 07 2013
prev sibling parent reply "Ross Hays" <throwaway email.net> writes:
 Strange. I'm getting a different error, but I'm still running 
 2.063.2.
 The error I get is
 `Error: cannot resolve type for t.opDispatch!("x")`
 What version are you running?
I just updated to 2.064.2
 In any case, the reason apparently is multifold:
 1. Apparently the proper error message isn't shown when using 
 the property notation. (I'd have to check to see if it happens 
 in 2.064 ... might be a fixed bug)
 2. `.charAt(0)` doesn't exist for D's strings. You can just use 
 bracket notation to access the index.
 3. When args is empty (as it will be for a getter, when you 
 call) args[0] doesn't exist, so `Error: array index [0] is 
 outside array bounds [0 .. 0]`

 So fix 2 and 3 and it works for getting x. The reason I use a 
 static if is to separate the cases where args has items and 
 when it does not (since args[0] is invalid when args.length == 
 0), so that'll be necessary to get it to work.
Okay 2 is fixed, leftovers of other languages in my mind. Also took care of 3 I think but I may still be missing something. Here is what I have now.. class Vector(int N, T) if (N <= 3) { T[N] data; this() { data[] = 0; } property ref T opDispatch(string fieldName, Args ...)(Args args) if (Args.length < 2 && fieldName.length == 1 && toOffset(fieldName) < N) { int offset = fieldName[0 .. 1] - 'x'; if (args.length != 0) return data[offset]; else return data[offset] = args[0]; } } Same error.
Nov 07 2013
next sibling parent "Ross Hays" <throwaway email.net> writes:
On Friday, 8 November 2013 at 04:28:31 UTC, Ross Hays wrote:
 Strange. I'm getting a different error, but I'm still running 
 2.063.2.
 The error I get is
 `Error: cannot resolve type for t.opDispatch!("x")`
 What version are you running?
I just updated to 2.064.2
 In any case, the reason apparently is multifold:
 1. Apparently the proper error message isn't shown when using 
 the property notation. (I'd have to check to see if it happens 
 in 2.064 ... might be a fixed bug)
 2. `.charAt(0)` doesn't exist for D's strings. You can just 
 use bracket notation to access the index.
 3. When args is empty (as it will be for a getter, when you 
 call) args[0] doesn't exist, so `Error: array index [0] is 
 outside array bounds [0 .. 0]`

 So fix 2 and 3 and it works for getting x. The reason I use a 
 static if is to separate the cases where args has items and 
 when it does not (since args[0] is invalid when args.length == 
 0), so that'll be necessary to get it to work.
Okay 2 is fixed, leftovers of other languages in my mind. Also took care of 3 I think but I may still be missing something. Here is what I have now.. class Vector(int N, T) if (N <= 3) { T[N] data; this() { data[] = 0; } property ref T opDispatch(string fieldName, Args ...)(Args args) if (Args.length < 2 && fieldName.length == 1 && toOffset(fieldName) < N) { int offset = fieldName[0 .. 1] - 'x'; if (args.length != 0) return data[offset]; else return data[offset] = args[0]; } } Same error.
Just reread and realized I glossed over your mention of static if. class Vector(int N, T) if (N <= 3) { T[N] data; this() { data[] = 0; } property ref T opDispatch(string fieldName, Args ...)(Args args) if (Args.length < 2 && fieldName.length == 1 && toOffset(fieldName) < N) { int offset = fieldName[0 .. 1] - 'x'; static if (args.length != 0) return data[offset]; else return data[offset] = args[0]; } } Still the same problems.
Nov 07 2013
prev sibling parent reply "Chris Cain" <clcain uncg.edu> writes:
On Friday, 8 November 2013 at 04:28:31 UTC, Ross Hays wrote:
 class Vector(int N, T) if (N <= 3) {
     T[N] data;

     this()
     {
     	data[] = 0;
     }

      property ref T opDispatch(string fieldName, Args ...)(Args 
 args)
         if (Args.length < 2 && fieldName.length == 1 && 
 toOffset(fieldName) < N)
     {
 	    int offset = fieldName[0 .. 1] - 'x';
 	    if (args.length != 0)
 	    	return data[offset];
 	    else
 	    	return data[offset] = args[0];
     }
 }

 Same error.
Sorry, I forgot to mention in that post that you have "toOffset" in your template constraint, which means it will also never match. You'll have to define it or replace it with `fieldName[0] - 'x';` Also, you might not want to do `fieldName[0 .. 1]` because that's a slice (which is just another array of length 1). It won't do what you're expecting.
Nov 07 2013
parent reply "Ross Hays" <throwaway email.net> writes:
 Sorry, I forgot to mention in that post that you have 
 "toOffset" in your template constraint, which means it will 
 also never match. You'll have to define it or replace it with 
 `fieldName[0] - 'x';`

 Also, you might not want to do `fieldName[0 .. 1]` because 
 that's a slice (which is just another array of length 1). It 
 won't do what you're expecting.
And more stupid mistakes on my part. Thank you, still not 100% there with D but working on it. (though some of those mistakes were dumb even outside of D lol). class Vector(int N, T) if (N <= 3) { T[N] data; property ref T opDispatch(string fieldName, Args ...)(Args args) if (Args.length < 2 && fieldName.length == 1 && fieldName[0] - 'x' < N) { int offset = fieldName[0] - 'x'; static if (args.length != 0) return data[offset]; else return data[offset] = args[0]; } } void main() { Vector!(2, float) t = new Vector!(2,float)(); writeln(t.x); } Another revision, still the same problems and I am pretty sure I am not forgetting anything this time... I am sure that is wrong too
Nov 07 2013
parent reply "Chris Cain" <clcain uncg.edu> writes:
On Friday, 8 November 2013 at 04:38:23 UTC, Ross Hays wrote:
 Thank you
No problem. I'm glad we're making progress on it. And don't feel bad about mistakes while learning. They happen. Embrace them because they happen to all of us at first especially when we're juggling learning new syntaxes and tricks and such.
 class Vector(int N, T) if (N <= 3) {
     T[N] data;

      property ref T opDispatch(string fieldName, Args ...)(Args 
 args)
         if (Args.length < 2 && fieldName.length == 1 && 
 fieldName[0] - 'x' < N)
     {
 	    int offset = fieldName[0] - 'x';
 	    static if (args.length != 0)
 	    	return data[offset];
 	    else
 	    	return data[offset] = args[0];
     }
 }
Okay, your static if's condition is swapped (args.length should be 0 if you're just returning data[offset]). Also, when you correct that, you'll run into an issue where it reveals that `data[offset] = args[0]` is not an lvalue (which is required for ref). Either change the return type to "auto" and remove the return for that branch... or, alternatively, you can split the statement into two: else { data[offset] = args[0]; return data[offset]; } and `data[offset]` will be an lvalue.
Nov 07 2013
next sibling parent reply "Ross Hays" <throwaway email.net> writes:
Boom, that last few were the issues. I elected to just move the 
return for the setter onto a separate line, mostly because the 
idea of auto returning different types seem foreign to me... I 
have used auto plenty in C++11, but never like that and it just 
throws me off. But fixing those other mistakes we get:

class Vector(int N, T) if (N <= 3) {
     T[N] data;

     this()
     {
     	data[] = 0;
     }

      property ref T opDispatch(string fieldName, Args ...)(Args 
args)
         if (Args.length < 2 && fieldName.length == 1 && 
fieldName[0] - 'x' < N)
     {
	    int offset = fieldName[0] - 'x';
	    static if (args.length == 0)
	    {
	    	return data[offset];
         }
	    else
	    {
	    	data[offset] = args[0];
	    	return data[offset];
         }
     }
}

void main()
{
	Vector!(2, float) t = new Vector!(2,float)();
	writeln(t.x);
	t.x = 4;
	writeln(t.x);
}

That works as intended, now if only it were useful.
Nov 07 2013
parent reply "Ross Hays" <throwaway email.net> writes:
And let me say that I really do like that this works in D. I 
can't imagine doing anything like this in C++ (which is what I 
used primarily in the past).

The only reason I joke about it being useless is it really only 
supports vectors of length 2 or 3 (technically 1 as well but that 
is not really a vector) and at that point I am tempted to say it 
is more efficient to just make a Vector2!float class or something 
of the sorts.
Nov 07 2013
parent reply Philippe Sigaud <philippe.sigaud gmail.com> writes:
On Fri, Nov 8, 2013 at 5:55 AM, Ross Hays <throwaway email.net> wrote:

 And let me say that I really do like that this works in D. I can't imagine
 doing anything like this in C++ (which is what I used primarily in the
 past).

 The only reason I joke about it being useless is it really only supports
 vectors of length 2 or 3 (technically 1 as well but that is not really a
 vector) and at that point I am tempted to say it is more efficient to just
 make a Vector2!float class or something of the sorts.
Then the next step is to allow swizzling (sp?) :-) auto myVec = yourVec.yzx; // myVec.x = yourVec.y, myVec.y = yourVec.z, myVec.z = yourVec.x; auto myVec2 = yourVec.xxx; // and so on
Nov 07 2013
parent =?UTF-8?B?QWxpIMOHZWhyZWxp?= <acehreli yahoo.com> writes:
On 11/07/2013 10:05 PM, Philippe Sigaud wrote:
 On Fri, Nov 8, 2013 at 5:55 AM, Ross Hays <throwaway email.net> wrote:

 And let me say that I really do like that this works in D. I can't imagine
 doing anything like this in C++ (which is what I used primarily in the
 past).

 The only reason I joke about it being useless is it really only supports
 vectors of length 2 or 3 (technically 1 as well but that is not really a
 vector) and at that point I am tempted to say it is more efficient to just
 make a Vector2!float class or something of the sorts.
Then the next step is to allow swizzling (sp?) :-) auto myVec = yourVec.yzx; // myVec.x = yourVec.y, myVec.y = yourVec.z, myVec.z = yourVec.x; auto myVec2 = yourVec.xxx; // and so on
Manu Evans had reported either during his talk at DConf 2013 or during private conversations at the same conference that the technique has been used at Remedy Games. Ali
Nov 25 2013
prev sibling parent reply "Ross Hays" <throwaway email.net> writes:
Okay here is something I was hoping for some general 
clarification on related to this and maybe you can help me sort 
some things out.

The opDispatch method has a template parameter of string 
fieldName. In C++, templates are actually compiled so each 
different use of a template class is compiled into its own thing. 
With D, if this is the case I imagine I would not be able to 
access the template parameter of string? If I can then why does 
it need to be a template parameter at all and not a normal 
parameter.
Nov 07 2013
parent reply "Chris Cain" <clcain uncg.edu> writes:
On Friday, 8 November 2013 at 05:10:57 UTC, Ross Hays wrote:
 Okay here is something I was hoping for some general 
 clarification on related to this and maybe you can help me sort 
 some things out.

 The opDispatch method has a template parameter of string 
 fieldName. In C++, templates are actually compiled so each 
 different use of a template class is compiled into its own 
 thing. With D, if this is the case I imagine I would not be 
 able to access the template parameter of string? If I can then 
 why does it need to be a template parameter at all and not a 
 normal parameter.
Yes, D is the same as C++ in that each unique template instantiation is compiled differently. Almost like copy & paste where you replace the template parameters with what was passed in. What do you mean by "not be able to access the template parameter of string"? If you just mean whether you can use it or not, well, you can. It's available to you both at compile time and at run time if it's a template parameter like that. In the case of opDispatch, the compiler essentially rewrites all unknown calls (if `x.foo()` doesn't exist, then change it to `x.opDispatch!"foo"()`). One of the reasons why passing it in as a template parameter is better than a regular parameter is that opDispatch!s can specialize at compile time so that it doesn't result in any runtime performance hit. You can look at it like this: with a compile-time opDispatch means that `opDispatch!"x"; opDispatch!"y";` and so on is (essentially) binary equivalent to you having actually hand-written those functions (that is just a hand-written `x` or `y` property). So, there's no runtime overhead of conditionals, no allocating strings, no actual passing of parameters, or, potentially, even the creation of that offset variable (so, it doesn't even do the subtraction at runtime because it can figure it out at compile time and just fill it in as if it were a literal). One tiny place of improvement for your code, however, is if you changed it to `static immutable offset = ...;` because that helps the compiler know to do that operation at compile time. That said, a Sufficiently Smart Compiler (tm) would be able to do it regardless of the extra hint, but AFAIK DMD typically doesn't do all of the optimizations it could.
Nov 07 2013
parent "bearophile" <bearophileHUGS lycos.com> writes:
Chris Cain:

 One tiny place of improvement for your code, however, is if you 
 changed it to `static immutable offset = ...;` because that 
 helps the compiler know to do that operation at compile time.
The idiomatic way to do that is to use "enum offset = ...;". Bye, bearophile
Nov 08 2013
prev sibling parent "Chris Cain" <clcain uncg.edu> writes:
On Friday, 8 November 2013 at 03:35:34 UTC, Ross Hays wrote:
 Awesome that seems to do what I was going for. I had tried a 
 similar approach with  property dispatch and the subtraction of 
 'x', but I had left out the static if and had the opDispatch 
 returning a ref of the entry in the array (so there would just 
 be the one  property still) but that resulted in "Error: no 
 property 'x' for type 'test.Vector!(2, float).Vector'".

 This is interesting, though probably not a very safe way to 
 handle vectors in the real world (even more so if they are 
 going to be vectors of more than length 3).
Indeed. Presumably you could modify it to shift the starting letter (which is currently hardcoded as 'x') leftward as N grows bigger to allow it to support more. But really, you would probably be better to just use tuples. Technically a Vector is just a Tuple!(T, "x", T, "y") so you could probably do better by writing some metaprogramming to make one of those if you wanted a short-hand for it. Otherwise just doing "alias Vector2(T) = Tuple!(T, "x", T, "y");" would work great in 2.064.2, if I remember the syntax correctly. Then Vector2!float would work. Similarly Vector3 could be made and so on. http://dlang.org/changelog.html#eponymous_template
Nov 07 2013