www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - Indexing with an arbitrary type

reply Alex <sascha.orlov gmail.com> writes:
Hi everybody,
I have a question and a half on templates and ranges, this time.
Say, I have two functions:

auto f1(T, S)(T t, S s) if(isIntegral!T && isRandomAccessRange!S)
{
     return s[t];
}

auto f2(T, S)(T t, RandomAccessFinite!S raf) if(isIntegral!T)
{
     return raf[t];
}

and a
class myClass : RandomAccessFinite!ubyte {...}
which implements all the methods needed by the RandomAccessFinite 
interface.

then, I could use this in my main by just

void main()
{
     myClass mC = new myClass();
     writeln(f2(1, mC));
     writeln(f1(1, mC));

     ubyte[] arr = [0, 42, 2, 3, 4];
     writeln(f1(1, arr));
     //writeln(f2(1, arr)); //this fails and is the first part of 
the question.
}

so, the first question is, why the method using the 
RandomAccessFinite interface fails on using an array? Did I miss 
a method, which is not supported by an array?

But the main question is about the other part, about the 
constraint to the first parameter to my functions.
It seems strange to me to use "isIntegral" here, as this is some 
kind of unrelated for something used as an index.
Is there anything more appropriate to check? What kind of 
interface/trait has an index to fulfill?
Aug 01 2016
parent reply Jonathan M Davis via Digitalmars-d-learn writes:
On Monday, August 01, 2016 11:06:54 Alex via Digitalmars-d-learn wrote:
 Hi everybody,
 I have a question and a half on templates and ranges, this time.
 Say, I have two functions:

 auto f1(T, S)(T t, S s) if(isIntegral!T && isRandomAccessRange!S)
 {
      return s[t];
 }

 auto f2(T, S)(T t, RandomAccessFinite!S raf) if(isIntegral!T)
 {
      return raf[t];
 }

 and a
 class myClass : RandomAccessFinite!ubyte {...}
 which implements all the methods needed by the RandomAccessFinite
 interface.

 then, I could use this in my main by just

 void main()
 {
      myClass mC = new myClass();
      writeln(f2(1, mC));
      writeln(f1(1, mC));

      ubyte[] arr = [0, 42, 2, 3, 4];
      writeln(f1(1, arr));
      //writeln(f2(1, arr)); //this fails and is the first part of
 the question.
 }

 so, the first question is, why the method using the
 RandomAccessFinite interface fails on using an array? Did I miss
 a method, which is not supported by an array?
An array does not implement RandomAccessFinite, which is an interface that you created. So, a function that takes a RandomAccessFinite is not going to accept an array. A dynamic array will match isRandomAccessRange, but that has nothing to do with interfaces.
 But the main question is about the other part, about the
 constraint to the first parameter to my functions.
 It seems strange to me to use "isIntegral" here, as this is some
 kind of unrelated for something used as an index.
 Is there anything more appropriate to check? What kind of
 interface/trait has an index to fulfill?
What's strange? If you want to accept any integer type, then isIntegral!T would do it. A _better_ thing to do would be to make it so that it's just size_t and not templatize the type, since indices really should be size_t normally (and if the system is 32-bit, then isIntegral!T will accept long and ulong, whereas size_t is uint, and if you pass a long, you'll get a compilation error for arrays, since they take size_t for indexing; it won't matter for 64-bit though, since size_t is ulong there). So, auto f1(R)(size_t index, R range) if(isRandomAccessRange!R) { return range[index]; } would be better, but aside from the 32-bit issues, isIntegral will work. - Jonathan M Davis
Aug 01 2016
parent reply Alex <sascha.orlov gmail.com> writes:
On Monday, 1 August 2016 at 13:52:56 UTC, Jonathan M Davis wrote:
 An array does not implement RandomAccessFinite, which is an 
 interface that you created. So, a function that takes a 
 RandomAccessFinite is not going to accept an array. A dynamic 
 array will match isRandomAccessRange, but that has nothing to 
 do with interfaces.
It's ok for me to say, that some types do not implement an interface to show some abilities, even if the interface, which has to be implemented to show the same abilities is given/known/public/exposed... etc... So, I think this part is ok now, I think...
 But the main question is about the other part, about the
 constraint to the first parameter to my functions.
 It seems strange to me to use "isIntegral" here, as this is 
 some
 kind of unrelated for something used as an index.
 Is there anything more appropriate to check? What kind of
 interface/trait has an index to fulfill?
What's strange? If you want to accept any integer type, then isIntegral!T would do it. A _better_ thing to do would be to make it so that it's just size_t and not templatize the type, since indices really should be size_t normally (and if the system is 32-bit, then isIntegral!T will accept long and ulong, whereas size_t is uint, and if you pass a long, you'll get a compilation error for arrays, since they take size_t for indexing; it won't matter for 64-bit though, since size_t is ulong there). So, auto f1(R)(size_t index, R range) if(isRandomAccessRange!R) { return range[index]; } would be better, but aside from the 32-bit issues, isIntegral will work. - Jonathan M Davis
This goes in a different direction I want to. I don't have anything against simplification to size_t, indeed I have it in this way currently. But what I want is something like the following: having alias MyIndex = int and MyIndex s = MyInd(1); writeln(f1(s, arr)); //gives 42, as expected Now, I want to define a struct S { int index; ??? what else ??? // alias index this; doesn't help } and still being able to have alias MyIndex = S and nothing else should be changed.
Aug 01 2016
parent reply Jonathan M Davis via Digitalmars-d-learn writes:
On Monday, August 01, 2016 14:46:03 Alex via Digitalmars-d-learn wrote:
 On Monday, 1 August 2016 at 13:52:56 UTC, Jonathan M Davis wrote:
 An array does not implement RandomAccessFinite, which is an
 interface that you created. So, a function that takes a
 RandomAccessFinite is not going to accept an array. A dynamic
 array will match isRandomAccessRange, but that has nothing to
 do with interfaces.
It's ok for me to say, that some types do not implement an interface to show some abilities, even if the interface, which has to be implemented to show the same abilities is given/known/public/exposed... etc... So, I think this part is ok now, I think...
I'm afraid that I don't know what you're talking about, but if what you have is working, then great. But while arrays can pass template constraints, they can't implement interfaces, so what you had before didn't really seem like it was going to work.
 But the main question is about the other part, about the
 constraint to the first parameter to my functions.
 It seems strange to me to use "isIntegral" here, as this is
 some
 kind of unrelated for something used as an index.
 Is there anything more appropriate to check? What kind of
 interface/trait has an index to fulfill?
What's strange? If you want to accept any integer type, then isIntegral!T would do it. A _better_ thing to do would be to make it so that it's just size_t and not templatize the type, since indices really should be size_t normally (and if the system is 32-bit, then isIntegral!T will accept long and ulong, whereas size_t is uint, and if you pass a long, you'll get a compilation error for arrays, since they take size_t for indexing; it won't matter for 64-bit though, since size_t is ulong there). So, auto f1(R)(size_t index, R range) if(isRandomAccessRange!R) { return range[index]; } would be better, but aside from the 32-bit issues, isIntegral will work. - Jonathan M Davis
This goes in a different direction I want to. I don't have anything against simplification to size_t, indeed I have it in this way currently. But what I want is something like the following: having alias MyIndex = int and MyIndex s = MyInd(1); writeln(f1(s, arr)); //gives 42, as expected Now, I want to define a struct S { int index; ??? what else ??? // alias index this; doesn't help } and still being able to have alias MyIndex = S and nothing else should be changed.
If you want a template constraint that checks that a type works with the index operator on a type, and you're not restricting it to something like size_t, then you're just going to have to check whether the expression is going to compile. Also, that is incompatible with random access ranges. Random access ranges are expected to work with size_t, and while isRandomAccessRange isn't currently quite that restrictive, it does check that r[1] compiles, so the index operator is going to have to accept integers at minimum. If you want a completely generic index operator, and you want a template constraint for it, you're pretty much going to have to test that it compiles. e.g. auto fun(I, T)(I index, T obj) if(__traits(compiles, obj[index])) { return obj[index]; } or if you want a reusable template, you can do something like template isIndexable(I, T) { enum isIndexable = __traits(compiles, T.init[I.init]); } auto fun(I, T)(I index, T obj) if(isIndexable!(I, T)) { return obj[index]; } Personally, I think that having code that accepts any type that can be indexable with any type isn't a particularly good idea, because odds are, it won't actually work, because those different types will work quite differently (e.g. associative arrays are indexable with stuff other than size_t, but they're really not interchangeable with dynamic arrays or static arrays, which use size_t). And anything that wants to be interchangeable with a dynamic array or be usable as a random access range should be using size_t to index. But if you actually have code where it makes sense to deal with types that can take all kinds of random stuff as indices, then something like I posted above should work. - Jonathan M Davis
Aug 01 2016
parent reply Alex <sascha.orlov gmail.com> writes:
On Monday, 1 August 2016 at 15:06:54 UTC, Jonathan M Davis wrote:
 If you want a template constraint that checks that a type works 
 with the index operator on a type, and you're not restricting 
 it to something like size_t, then you're just going to have to 
 check whether the expression is going to compile. Also, that is 
 incompatible with random access ranges. Random access ranges 
 are expected to work with size_t, and while isRandomAccessRange 
 isn't currently quite that restrictive, it does check that r[1] 
 compiles, so the index operator is going to have to accept 
 integers at minimum. If you want a completely generic index 
 operator, and you want a template constraint for it, you're 
 pretty much going to have to test that it compiles. e.g.

 auto fun(I, T)(I index, T obj)
     if(__traits(compiles, obj[index]))
 {
     return obj[index];
 }

 or if you want a reusable template, you can do something like

 template isIndexable(I, T)
 {
     enum isIndexable = __traits(compiles, T.init[I.init]);
 }

 auto fun(I, T)(I index, T obj)
     if(isIndexable!(I, T))
 {
     return obj[index];
 }

 Personally, I think that having code that accepts any type that 
 can be indexable with any type isn't a particularly good idea, 
 because odds are, it won't actually work, because those
 different types will work quite differently (e.g. associative 
 arrays are indexable with stuff other than size_t, but they're 
 really not interchangeable with dynamic arrays or static 
 arrays, which use size_t). And anything that wants to be 
 interchangeable with a dynamic array or be usable as a random 
 access range should be using size_t to index. But if you
This is exactly what my question is about. I don't think, that it doesn't make sense (and isn't a good idea) to index with an arbitrary type, too. So, how I can define/design a type, which is not an int/size_t, but has one to be able to index with it? And if the __compiles trait is the way to go with... well then that's just how it is...
Aug 01 2016
parent reply Jonathan M Davis via Digitalmars-d-learn writes:
On Monday, August 01, 2016 15:25:59 Alex via Digitalmars-d-learn wrote:
 On Monday, 1 August 2016 at 15:06:54 UTC, Jonathan M Davis wrote:
 If you want a template constraint that checks that a type works
 with the index operator on a type, and you're not restricting
 it to something like size_t, then you're just going to have to
 check whether the expression is going to compile. Also, that is
 incompatible with random access ranges. Random access ranges
 are expected to work with size_t, and while isRandomAccessRange
 isn't currently quite that restrictive, it does check that r[1]
 compiles, so the index operator is going to have to accept
 integers at minimum. If you want a completely generic index
 operator, and you want a template constraint for it, you're
 pretty much going to have to test that it compiles. e.g.

 auto fun(I, T)(I index, T obj)
     if(__traits(compiles, obj[index]))
 {
     return obj[index];
 }

 or if you want a reusable template, you can do something like

 template isIndexable(I, T)
 {
     enum isIndexable = __traits(compiles, T.init[I.init]);
 }

 auto fun(I, T)(I index, T obj)

     if(isIndexable!(I, T))
 {
     return obj[index];
 }

 Personally, I think that having code that accepts any type that
 can be indexable with any type isn't a particularly good idea,
 because odds are, it won't actually work, because those

 different types will work quite differently (e.g. associative
 arrays are indexable with stuff other than size_t, but they're
 really not interchangeable with dynamic arrays or static
 arrays, which use size_t). And anything that wants to be
 interchangeable with a dynamic array or be usable as a random
 access range should be using size_t to index. But if you
This is exactly what my question is about. I don't think, that it doesn't make sense (and isn't a good idea) to index with an arbitrary type, too.
The issue is that the semantics have to be sufficiently similar across the types that the code will work. And if your code is doing much beyond indexing, it probably won't. But if it does in your case, then great.
 So, how I can define/design a type, which is not an int/size_t,
 but has one to be able to index with it?

 And if the __compiles trait is the way to go with... well then
 that's just how it is...
You're pretty much going to have to go with some form of __traits(compiles, ...) to test the code that the code that you want to code compiles. That's ultimately what a lot of traits do. And if what you're looking to test is that obj[index] works where obj and index are arbitrary types, what else would you be testing for anyway? You'd only need to test for more beyond that if you were trying to further restrict the types involved and/or to require that some other set of operations compiled in addition to indexing. - Jonathan M Davis
Aug 01 2016
next sibling parent Alex <sascha.orlov gmail.com> writes:
On Monday, 1 August 2016 at 15:51:58 UTC, Jonathan M Davis wrote:
 On Monday, August 01, 2016 15:25:59 Alex via 
 Digitalmars-d-learn wrote:
 On Monday, 1 August 2016 at 15:06:54 UTC, Jonathan M Davis 
 wrote:
 If you want a template constraint that checks that a type 
 works with the index operator on a type, and you're not 
 restricting it to something like size_t, then you're just 
 going to have to check whether the expression is going to 
 compile. Also, that is incompatible with random access 
 ranges. Random access ranges are expected to work with 
 size_t, and while isRandomAccessRange isn't currently quite 
 that restrictive, it does check that r[1] compiles, so the 
 index operator is going to have to accept integers at 
 minimum. If you want a completely generic index operator, 
 and you want a template constraint for it, you're pretty 
 much going to have to test that it compiles. e.g.

 auto fun(I, T)(I index, T obj)
     if(__traits(compiles, obj[index]))
 {
     return obj[index];
 }

 or if you want a reusable template, you can do something like

 template isIndexable(I, T)
 {
     enum isIndexable = __traits(compiles, T.init[I.init]);
 }

 auto fun(I, T)(I index, T obj)

     if(isIndexable!(I, T))
 {
     return obj[index];
 }

 Personally, I think that having code that accepts any type 
 that can be indexable with any type isn't a particularly 
 good idea, because odds are, it won't actually work, because 
 those

 different types will work quite differently (e.g. 
 associative arrays are indexable with stuff other than 
 size_t, but they're really not interchangeable with dynamic 
 arrays or static arrays, which use size_t). And anything 
 that wants to be interchangeable with a dynamic array or be 
 usable as a random access range should be using size_t to 
 index. But if you
This is exactly what my question is about. I don't think, that it doesn't make sense (and isn't a good idea) to index with an arbitrary type, too.
The issue is that the semantics have to be sufficiently similar across the types that the code will work. And if your code is doing much beyond indexing, it probably won't. But if it does in your case, then great.
 So, how I can define/design a type, which is not an 
 int/size_t, but has one to be able to index with it?

 And if the __compiles trait is the way to go with... well then 
 that's just how it is...
You're pretty much going to have to go with some form of __traits(compiles, ...) to test the code that the code that you want to code compiles. That's ultimately what a lot of traits do. And if what you're looking to test is that obj[index] works where obj and index are arbitrary types, what else would you be testing for anyway? You'd only need to test for more beyond that if you were trying to further restrict the types involved and/or to require that some other set of operations compiled in addition to indexing. - Jonathan M Davis
Got it... Thanks :)
Aug 01 2016
prev sibling parent reply Alex <sascha.orlov gmail.com> writes:
On Monday, 1 August 2016 at 16:09:50 UTC, Alex wrote:
 On Monday, 1 August 2016 at 15:51:58 UTC, Jonathan M Davis 
 wrote:
 template isIndexable(I, T)
 {
     enum isIndexable = __traits(compiles, T.init[I.init]);
 }
As a last question afterwards: Is it possible to create such an isIndexable template without templating over the type T? something like template isIndexable(I) { enum isIndexable = __traits(compiles, ???[I.init]); } sure, I could use __traits(compiles, (int[]).init[I.init]) but is this the intended way to go?
Aug 03 2016
parent reply Jonathan M Davis via Digitalmars-d-learn writes:
On Wednesday, August 03, 2016 09:21:13 Alex via Digitalmars-d-learn wrote:
 On Monday, 1 August 2016 at 16:09:50 UTC, Alex wrote:
 On Monday, 1 August 2016 at 15:51:58 UTC, Jonathan M Davis
 wrote:
 template isIndexable(I, T)
 {

     enum isIndexable = __traits(compiles, T.init[I.init]);

 }
As a last question afterwards: Is it possible to create such an isIndexable template without templating over the type T? something like template isIndexable(I) { enum isIndexable = __traits(compiles, ???[I.init]); } sure, I could use __traits(compiles, (int[]).init[I.init]) but is this the intended way to go?
__traits(compiles, ...) is testing whether the code in question compiles. What you need to test is whether the object is indexable by the other, and that would mean needing both the type being indexed and the type which is the index. Just because one particular type is indexable by whatever the index type is doesn't mean that another will be. For instance, int[] is only going to be indexable by size_t and anything that implicitly converts to size_t. Testing whether int[] is indexable with size_t isn't going to say anything about whether int[string] is indexable with size_t. If you want to test that int[string] is indexable with size_t, you'll need to actually test it with size_t, not int[] - the same goes for any other combinaton of object to index and object that is the index. If you're just typing out the whole thing every time, then you can do stuff like auto foo(I, T)(I index, T obj) if(__traits(compiles, obj[index])) { ... } because you have actual objects to deal with, whereas with a template written to encapsulate that test, you only have the types. The init property is just an easy way to get at an object of the type without declaring it or worrying about how one is constructed. But to do the same test as that example with a separate template, you're going to need both types. e.g. template isIndexable(I, T) { enum isIndexable = __traits(compiles, T.init[I.init]); } If you don't have both, you're not doing the same test, and it's impossible to test that one type is indexable by another without using both types in the test. To only have one of the two types in a test would be like trying t to test whether a function can be called on a particular type while only having the function in the test and not the type. It doesn't work. - Jonathan M Davis
Aug 03 2016
parent reply Alex <sascha.orlov gmail.com> writes:
On Wednesday, 3 August 2016 at 14:23:59 UTC, Jonathan M Davis 
wrote:
 On Wednesday, August 03, 2016 09:21:13 Alex via 
 Digitalmars-d-learn wrote:
 On Monday, 1 August 2016 at 16:09:50 UTC, Alex wrote:
 On Monday, 1 August 2016 at 15:51:58 UTC, Jonathan M Davis
 wrote:
 template isIndexable(I, T)
 {

     enum isIndexable = __traits(compiles, T.init[I.init]);

 }
As a last question afterwards: Is it possible to create such an isIndexable template without templating over the type T? something like template isIndexable(I) { enum isIndexable = __traits(compiles, ???[I.init]); } sure, I could use __traits(compiles, (int[]).init[I.init]) but is this the intended way to go?
__traits(compiles, ...) is testing whether the code in question compiles. What you need to test is whether the object is indexable by the other, and that would mean needing both the type being indexed and the type which is the index. Just because one particular type is indexable by whatever the index type is doesn't mean that another will be. For instance, int[] is only going to be indexable by size_t and anything that implicitly converts to size_t. Testing whether int[] is indexable with size_t isn't going to say anything about whether int[string] is indexable with size_t. If you want to test that int[string] is indexable with size_t, you'll need to actually test it with size_t, not int[] - the same goes for any other combinaton of object to index and object that is the index. If you're just typing out the whole thing every time, then you can do stuff like auto foo(I, T)(I index, T obj) if(__traits(compiles, obj[index])) { ... } because you have actual objects to deal with, whereas with a template written to encapsulate that test, you only have the types. The init property is just an easy way to get at an object of the type without declaring it or worrying about how one is constructed. But to do the same test as that example with a separate template, you're going to need both types. e.g. template isIndexable(I, T) { enum isIndexable = __traits(compiles, T.init[I.init]); } If you don't have both, you're not doing the same test, and it's impossible to test that one type is indexable by another without using both types in the test. To only have one of the two types in a test would be like trying t to test whether a function can be called on a particular type while only having the function in the test and not the type. It doesn't work. - Jonathan M Davis
But it should. Just found this: https://www.sgi.com/tech/stl/StrictWeakOrdering.html which should be fulfilled by a type, which can be used as a key. So, in my opinion, the property "being a key" is a matter of an abstract definition and should have nothing to do with objects which I want to index with it. Sure, the object which I want to index, has to define an opIndex method, which can receive the key-thing, but this is already the next step.
Aug 03 2016
parent reply Alex <sascha.orlov gmail.com> writes:
On Wednesday, 3 August 2016 at 14:46:25 UTC, Alex wrote:
 On Wednesday, 3 August 2016 at 14:23:59 UTC, Jonathan M Davis 
 wrote:

 But it should.

 Just found this:
 https://www.sgi.com/tech/stl/StrictWeakOrdering.html
 which should be fulfilled by a type, which can be used as a 
 key. So, in my opinion, the property "being a key" is a matter 
 of an abstract definition and should have nothing to do with 
 objects which I want to index with it.

 Sure, the object which I want to index, has to define an 
 opIndex method, which can receive the key-thing, but this is 
 already the next step.
So, I think this is the answer to my question: http://stackoverflow.com/questions/27282568/d-template-constraint-to-show-whether-a-given-type-is-comparable
Aug 03 2016
parent reply Jonathan M Davis via Digitalmars-d-learn writes:
On Wednesday, August 03, 2016 16:42:18 Alex via Digitalmars-d-learn wrote:
 On Wednesday, 3 August 2016 at 14:46:25 UTC, Alex wrote:
 On Wednesday, 3 August 2016 at 14:23:59 UTC, Jonathan M Davis
 wrote:

 But it should.

 Just found this:
 https://www.sgi.com/tech/stl/StrictWeakOrdering.html
 which should be fulfilled by a type, which can be used as a
 key. So, in my opinion, the property "being a key" is a matter
 of an abstract definition and should have nothing to do with
 objects which I want to index with it.

 Sure, the object which I want to index, has to define an
 opIndex method, which can receive the key-thing, but this is
 already the next step.
So, I think this is the answer to my question: http://stackoverflow.com/questions/27282568/d-template-constraint-to-show-wh ether-a-given-type-is-comparable
int is comparable, but it's not going to index float[string]. Only a string is going to do that. Similarly, long is comparable, but on a 32-bit system, it won't index int[], because it's larger than size_t, which is the actual index type for int[]. If all that your code cares about is that type T is comparable, then sure, test that it's comparable. But if it's going to pass a value of type T to the index operator of type U, then T's index operator needs to accept type U. And a type like SysTime from std.datetime is comparable, but it sure isn't going to work as an index for int[]. And being comparable isn't enough to guarantee that T is going to work. In fact, there isn't even a guarantee that U's opIndex even takes a type that's comparable. Maybe it's a weird socket type that use opIndex to connect to a particular IP address rather than having a properly named function for that, and it takes an Address object that isn't comparable. Yes, that would be weird and likely bad design, but there's nothing about opIndex that prevents it. So, testing for whether U was comparable wouldn't necessarily tell you whether it could be used to index a type. But even if everyone restricts themselves to using opIndex in sane ways, and all index types are comparable, not all types take the same index types. So, if you just test that the index type is comparable without testing it against the type that it's actually going to index, then your template constraint will let through types that won't be compatible and will result in a compilation error. Maybe I don't understand enough about what you're trying to do and am not giving the best advice as a result, but if you have a function that takes a two arguments where one is supposed to be an index, and the other is supposed to be an object to be indexed, then the only template constraint that will prevent types getting through which won't compile when the code actually uses the index operator is to test that the object to be indexed can be indexed by the index object. Testing that the index type is comparable is completely insufficient. It will work with a particular set of types, but it will not work with all indexable types, and if a types that don't work together are passed to that function, then you will get a compilation error inside of the function instead of at the template constraint catching it. - Jonathan M Davis
Aug 03 2016
parent reply Alex <sascha.orlov gmail.com> writes:
On Wednesday, 3 August 2016 at 17:56:54 UTC, Jonathan M Davis 
wrote:
 int is comparable, but it's not going to index float[string]. 
 Only a string is going to do that. Similarly, long is 
 comparable, but on a 32-bit system, it won't index int[], 
 because it's larger than size_t, which is the actual index type 
 for int[].

 If all that your code cares about is that type T is comparable, 
 then sure, test that it's comparable. But if it's going to pass 
 a value of type T to the index operator of type U, then T's 
 index operator needs to accept type U. And a type like SysTime 
 from std.datetime is comparable, but it sure isn't going to 
 work as an index for int[].
This is a good point, I admit. But still, isn't it the indexable objects task to define the opIndex method to take a particular index?
 And being comparable isn't enough to guarantee that T is going 
 to work.
 In fact, there isn't even a guarantee that U's opIndex even 
 takes a type
 that's comparable. Maybe it's a weird socket type that use 
 opIndex to
 connect to a particular IP address rather than having a 
 properly named
 function for that, and it takes an Address object that isn't 
 comparable.
 Yes, that would be weird and likely bad design, but there's 
 nothing about
 opIndex that prevents it. So, testing for whether U was 
 comparable wouldn't
 necessarily tell you whether it could be used to index a type.
Yes. I tested a struct, which has a Nullable value in it aliased to this and with an opCmp defined with respect to the Nullable value and it didn't worked, too. I find this strange, because if I already define, how the value should behave, if it is in the null state, I would assume, it should be able to index with it too...
 But even if everyone restricts themselves to using opIndex in 
 sane ways, and all index types are comparable, not all types 
 take the same index types. So, if you just test that the index 
 type is comparable without testing it against the type that 
 it's actually going to index, then your template constraint 
 will let through types that won't be compatible and will result 
 in a compilation error.
Yes. But this is just a compilation error... It says, well, there is a mistake in the code. What I want is to achieve a constraint on my own type, which is at this time point defined just as alias MyType = size_t; but later on, this could maybe become a struct, and I still want to be able to handle it like an index. The opIndex methods of the surrounding objects are already defined in terms of the MyType, so the problem of interacting does not play a big role. The question is how to describe the minimal requirement on the type itself...
 Maybe I don't understand enough about what you're trying to do 
 and am not giving the best advice as a result, but if you have 
 a function that takes a two arguments where one is supposed to 
 be an index, and the other is supposed to be an object to be 
 indexed, then the only template constraint that will prevent 
 types getting through which won't compile when the code 
 actually uses the index operator is to test that the object to 
 be indexed can be indexed by the index object. Testing that the 
 index type is comparable is completely insufficient.
Yes... I can confirm this... the last line of this code inside the main gives an error... https://dpaste.dzfl.pl/043d1deb9073
 It will work with a particular set of types, but it will not 
 work with all indexable types, and if a types that don't work 
 together are passed to that function, then you will get a 
 compilation error inside of the function instead of at the 
 template constraint catching it.
The code above, gives even a runtime error, which is much worse... It would be enough, I think, to assert, that MyType resolves to the particular set of types... Which are these? Is it possible to write this kind of template?
Aug 03 2016
parent reply Alex <sascha.orlov gmail.com> writes:
What I think about is something like this:
https://dpaste.dzfl.pl/d37cfb8e513d
Aug 04 2016
parent reply Jonathan M Davis via Digitalmars-d-learn writes:
On Thursday, August 04, 2016 08:13:59 Alex via Digitalmars-d-learn wrote:
 What I think about is something like this:
 https://dpaste.dzfl.pl/d37cfb8e513d
Okay, you have enum bool isKey(T) = is(typeof(T.init < T.init) : bool); template isNullable(T) { enum isNullable = __traits(compiles, T.init.isNull); } struct IndexAble { int[] arr; this(size_t size) { arr.length = size; } ref int opIndex(T)(T ind) if(isKey!T) { static if(isNullable!T) if(ind.isNull) return arr[$]; // or another manually defined state. return arr[ind]; } } What happens if I do this? void main() { IndexAble indexable; indexable["hello"]; } You get a compilation error like q.d(17): Error: cannot implicitly convert expression (ind) of type string to ulong q.d(24): Error: template instance q.IndexAble.opIndex!string error instantiating because the index type of int[] is size_t, so it's only going to accept values that are implicitly convertible to size_t, and string is not implicitly convertible to size_t. opIndex's template constraint did not catch the fact that passing "hello" to opIndex would result in a compilation error. string is perfectly comparable, and it's not a valid index type for int[] - and comparability is all that opIndex's template constraint checks for. For opIndex to actually catch that an argument is not going to compile if opIndex is instantiated with that type, then its template constraint needs to either test that the index type will implicitly convert to size_t so that it can be used to index the int[], or it needs to test that indexing int[] with the argument will compile. So, something like ref int opIndex(T)(T ind) if(is(T : size_t)) {...} or ref int opIndex(T)(T ind) if(__traits(compiles, arr[ind])) {...} And given that you're dealing explicitly with int[] and not an arbitrary type, it really makes more sense to use is(T : size_t). If IndexAble were templated on the type for arr, then you would need to do the second. But testing for comparability is completely insufficient for testing whether the type can be used as an index. In fact, it's pretty much irrelevant. What matters is whether the index type will compile with the code that it's being used in inside the template. If you're forwarding the index variable to another object's opIndex, then what matters is whether that opIndex will compile with the index, and if you're doing something else with it, then what matters is that it compiles with whatever it is that you're doing with it. I could declare a hash map implementation which did not use opCmp but just used opEquals and toHash. Then I could have a struct like struct S { int i; int j; size_t toHash() { return i * j % size_t.max; } } It has an opEquals (the implicit one declared by the compiler). It has a toHash. It does not have opCmp (and the compiler won't generate one). So, it's not comparable. But it would work in my hash map type only used toHash and opEquals and not opCmp. HashMap!(S, string) hm; hm[S(5, 22)] = "hello world"; And actually, while D's built-in AA's used to use opCmp (and thus required it), they don't anymore. So, that code even works with the built-in AA's. e.g. string[S] aa; aa[S(5, 22)] = "hello world"; So, comparability has nothing to do with whether a particular type will work as the key for an indexable type. What matters is that the index operator be given an argument that will compile with it - which usually means a type which is implicitly convertible to the type that it uses for its indices but in more complex implementations where opIndex might accept a variety of types for whatever reason, then what matters is whether the type that you want to pass it will compile with it. And unless all that that particular opIndex implentation needs to care about for a type to work with it is that the type is comparable, then testing that the type that you're going to give it is comparable is insufficient and may not even be relevant. - Jonathan M Davis
Aug 05 2016
parent Alex <sascha.orlov gmail.com> writes:
On Friday, 5 August 2016 at 16:37:14 UTC, Jonathan M Davis wrote:
 On Thursday, August 04, 2016 08:13:59 Alex via 
 Digitalmars-d-learn wrote:
 What I think about is something like this: 
 https://dpaste.dzfl.pl/d37cfb8e513d
Okay, you have enum bool isKey(T) = is(typeof(T.init < T.init) : bool); template isNullable(T) { enum isNullable = __traits(compiles, T.init.isNull); } struct IndexAble { int[] arr; this(size_t size) { arr.length = size; } ref int opIndex(T)(T ind) if(isKey!T) { static if(isNullable!T) if(ind.isNull) return arr[$]; // or another manually defined state. return arr[ind]; } } What happens if I do this? void main() { IndexAble indexable; indexable["hello"]; } You get a compilation error like q.d(17): Error: cannot implicitly convert expression (ind) of type string to ulong q.d(24): Error: template instance q.IndexAble.opIndex!string error instantiating because the index type of int[] is size_t, so it's only going to accept values that are implicitly convertible to size_t, and string is not implicitly convertible to size_t.
Yes. You are surely right.
 opIndex's template constraint did not catch the fact that 
 passing "hello" to opIndex would result in a compilation error. 
 string is perfectly comparable, and it's not a valid index type 
 for int[] - and comparability is all that opIndex's template 
 constraint checks for.
Right. My intension was to find the one check, which has to be fulfilled by all types, which could be used as an index...
 For opIndex to actually catch that an argument is not going to 
 compile if opIndex is instantiated with that type, then its 
 template constraint needs to either test that the index type 
 will implicitly convert to size_t so that it can be used to 
 index the int[],
Which would be too restrictive, as we have associative arrays...
 or it needs to test that indexing int[] with the argument will 
 compile. So, something like
I see the technical point, but the question which I want to answer with the isKey template is a different one. Maybe, a better formulation would be: "is a type uniquely convertible to size_t".
 ref int opIndex(T)(T ind)
     if(is(T : size_t))
 {...}

 or

 ref int opIndex(T)(T ind)
     if(__traits(compiles, arr[ind]))
 {...}

 And given that you're dealing explicitly with int[] and not an 
 arbitrary type, it really makes more sense to use is(T : 
 size_t). If IndexAble were templated on the type for arr, then 
 you would need to do the second. But testing for comparability 
 is completely insufficient for testing whether the type can be 
 used as an index.
I think, if I separate the concern of "can use sth. as key" and "how to use sth. as key" the isKey template is sufficient for the first one. Sure, without providing the answer for what...
 In fact, it's pretty much irrelevant. What matters is whether 
 the index type will compile with the code that it's being used 
 in inside the template. If you're forwarding the index variable 
 to another object's opIndex, then what matters is whether that 
 opIndex will compile with the index, and if you're doing 
 something else with it, then what matters is that it compiles 
 with whatever it is that you're doing with it.
Right. I just wanted to clarify for myself, what is the minimum requirement for a "key" is. Meanwhile, I'm close to the view, that it has to provide some conversion to size_t, providing toHash is maybe nothing else...
 I could declare a hash map implementation which did not use 
 opCmp but just used opEquals and toHash. Then I could have a 
 struct like

 struct S
 {
     int i;
     int j;

     size_t toHash()
     {
         return i * j % size_t.max;
     }
 }

 It has an opEquals (the implicit one declared by the compiler). 
 It has a toHash. It does not have opCmp (and the compiler won't 
 generate one). So, it's not comparable. But it would work in my 
 hash map type only used toHash and opEquals and not opCmp.

 HashMap!(S, string) hm;
 hm[S(5, 22)] = "hello world";

 And actually, while D's built-in AA's used to use opCmp (and 
 thus required it), they don't anymore. So, that code even works 
 with the built-in AA's. e.g.

 string[S] aa;
 aa[S(5, 22)] = "hello world";
Yeah. But you have either a perfect hashing and the absence of opCmp is the point what I'm wondering about. Or, the hashing is not perfect and then the usage of a key type relies on handling of collisions.
 So, comparability has nothing to do with whether a particular 
 type will work as the key for an indexable type. What matters 
 is that the index operator be given an argument that will 
 compile with it - which usually means a type which is 
 implicitly convertible to the type that it uses for its indices 
 but in more complex implementations where opIndex might accept 
 a variety of types for whatever reason, then what matters is 
 whether the type that you want to pass it will compile with it.
At the end - yes, I totally agree :)
 And unless all that that particular opIndex implentation needs 
 to care about for a type to work with it is that the type is 
 comparable, then testing that the type that you're going to 
 give it is comparable is insufficient and may not even be 
 relevant.
Ok, the relevance is a good point... And yes... you are probably right. As a complement: I surely can't define an opIndex operator on all types, which come through my isKey template. Nor, the template can check, if a type can be used as an index for something. Even if I make a templated opIndex, I would not know, how to handle all value, which comes as an input. I think, what the template is good for is to check, if it is possible to define an opIndex in the terms of a type at all, and providing a hash method is just a workaround for not providing a strict comparison.
Aug 06 2016