www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - I want to create my own Tuple type

reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
I want to create my own Tuple type, and I don't like the messy 
implementation of the one in D's standard lib, or the one in 
Clang++'s standard lib. The concept is very simple so it is 
disheartening if it cannot be done in a simple way that does not 
impact compile times... :-/

The requirements are:

1. It should not lead to slower compile times than fixed arity 
templates (if so, then I would just list all sizes from 0..64 
instead).

2. If all types are similar then it should be implemented as a 
static array with indexing. If not, then all fields should have 
the names __0, __1 etc.


Something along the lines of, but not with fixed arity:


struct Tuple(){}

struct Tuple(T0){
     T0[1] __v;
     const ref opIndex(size_t i){ return __v[i]; }
}

struct Tuple(T0,T1){
     static if (is(T0==T1)) {
         T0[2] __v;
         this(T0 a, T1 b){__v[0]=a; __v[1]=b; }
         const ref opIndex(size_t i){ return __v[i]; }
     } else {
         T0 __0;
         T1 __1;
     }
}

auto _tuple(Ts...)(Ts args){
     return __Tuple!Ts(args);
}
Jan 10
parent reply Paul Backus <snarwin gmail.com> writes:
On Sunday, 10 January 2021 at 16:52:30 UTC, Ola Fosheim Grøstad 
wrote:
 I want to create my own Tuple type, and I don't like the messy 
 implementation of the one in D's standard lib, or the one in 
 Clang++'s standard lib. The concept is very simple so it is 
 disheartening if it cannot be done in a simple way that does 
 not impact compile times... :-/

 The requirements are:
[...]
 2. If all types are similar then it should be implemented as a 
 static array with indexing. If not, then all fields should have 
 the names __0, __1 etc.
Why are these particular implementation details important to you?
Jan 10
parent reply Ola Fosheim Grostad <ola.fosheim.grostad gmail.com> writes:
On Monday, 11 January 2021 at 01:49:26 UTC, Paul Backus wrote:
 Why are these particular implementation details important to 
 you?
It is for object.d. I want to allow fast runtime indexing if all elements are of the same type. If the types are different I want static indexing, so the plan is to resolve failed lookup as __0 etc by modifying the compiler.
Jan 10
parent reply Paul Backus <snarwin gmail.com> writes:
On Monday, 11 January 2021 at 05:44:19 UTC, Ola Fosheim Grostad 
wrote:
 On Monday, 11 January 2021 at 01:49:26 UTC, Paul Backus wrote:
 Why are these particular implementation details important to 
 you?
It is for object.d. I want to allow fast runtime indexing if all elements are of the same type.
static if (allSameType) { auto opIndex(size_t i) { switch (i) { static foreach (j; 0 .. Types.length) { case j: return this.expand[j]; } default: assert(0); // or throw RangeError } } } Any decent compiler will turn this into `return this.expand[i]`.
 If the types are different I want static indexing, so the plan 
 is to resolve failed lookup as __0 etc by modifying the 
 compiler.
You can just fall back to `alias expand this` like Phobos's Tuple does in this case. No compiler modification needed.
Jan 10
parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Monday, 11 January 2021 at 05:59:03 UTC, Paul Backus wrote:
 static if (allSameType) {
     auto opIndex(size_t i) {
         switch (i) {
             static foreach (j; 0 .. Types.length) {
                 case j: return this.expand[j];
             }
             default: assert(0); // or throw RangeError
         }
     }
 }

 Any decent compiler will turn this into `return this.expand[i]`.
Maybe, I would then have to test for release mode and replace default: faulting with "unreachable"... I guess I won't know how this works out without testing, but it would in general be nice to be sure that a reinterpret cast to a static array would work.
 If the types are different I want static indexing, so the plan 
 is to resolve failed lookup as __0 etc by modifying the 
 compiler.
You can just fall back to `alias expand this` like Phobos's Tuple does in this case. No compiler modification needed.
I though maybe it would be nice in general to be able to create static indexed type by having a special field name pattern, but I will have a another look at staticMap (I don't really want the full staticMap into object.d though). I am a bit worried about compile time implications as I use tuples more and more in my own code, and expect that is a common trend... Maybe one could special case arity 0,1,2,3,4,5,6,7,8 and use a more generic template for 9 and up.
Jan 11
next sibling parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Monday, 11 January 2021 at 09:42:39 UTC, Ola Fosheim Grøstad 
wrote:
 I though maybe it would be nice in general to be able to create 
 static indexed type by having a special field name pattern, but 
 I will have a another look at staticMap (I don't really want 
 the full staticMap into object.d though).
This is the core of staticMap, I find it problematic to do this kind of string mixin in object.d: private enum staticMapExpandFactor = 150; private string generateCases() { string[staticMapExpandFactor] chunks; chunks[0] = q{}; static foreach (enum i; 0 .. staticMapExpandFactor - 1) chunks[i + 1] = chunks[i] ~ `F!(Args[` ~ i.stringof ~ `]),`; string ret = `AliasSeq!(`; foreach (chunk; chunks) ret ~= `q{alias staticMap = AliasSeq!(` ~ chunk ~ `);},`; return ret ~ `)`; } private alias staticMapBasicCases = AliasSeq!(mixin(generateCases()));
Jan 11
prev sibling parent reply Paul Backus <snarwin gmail.com> writes:
On Monday, 11 January 2021 at 09:42:39 UTC, Ola Fosheim Grøstad 
wrote:
 On Monday, 11 January 2021 at 05:59:03 UTC, Paul Backus wrote:
 You can just fall back to `alias expand this` like Phobos's 
 Tuple does in this case. No compiler modification needed.
I though maybe it would be nice in general to be able to create static indexed type by having a special field name pattern, but I will have a another look at staticMap (I don't really want the full staticMap into object.d though).
I have no idea why you're bringing up staticMap here. The simplest way to create a tuple's fields is to use type sequence instantiation [1], like Phobos does: struct Tuple(Types...) { Types expand; // ... } You can then access the individual fields with `expand[i]`, where `i` is any compile-time evaluable integer expression. Adding `alias expand this` (as Phobos does) allows the indexing to be performed directly on the tuple, without having to write `.expand` first. To handle dynamic vs. static indexing you can use a simple static if: static if (allSameTypes) { auto opIndex(size_t i) { // ... } } else { alias expand this; } [1] https://dlang.org/articles/ctarguments.html#type-seq-instantiation
Jan 11
next sibling parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Monday, 11 January 2021 at 14:03:39 UTC, Paul Backus wrote:
 On Monday, 11 January 2021 at 09:42:39 UTC, Ola Fosheim Grøstad 
 wrote:
 On Monday, 11 January 2021 at 05:59:03 UTC, Paul Backus wrote:
 You can just fall back to `alias expand this` like Phobos's 
 Tuple does in this case. No compiler modification needed.
I though maybe it would be nice in general to be able to create static indexed type by having a special field name pattern, but I will have a another look at staticMap (I don't really want the full staticMap into object.d though).
I have no idea why you're bringing up staticMap here. The simplest way to create a tuple's fields is to use type sequence instantiation [1], like Phobos does:
Ok, thanks. I guess typecons only use staticMap because it allows for named fields.
Jan 11
prev sibling parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Monday, 11 January 2021 at 14:03:39 UTC, Paul Backus wrote:
     alias expand this;
Hm... this does not allow me protect the fields from being changed. I also cannot use const since it is transitive and would make it impossible to return two references to mutable objects? Basically, the tuple itself should be immutable, but not the objects being referenced. I guess I could run over the types and add const if they are not references?
Jan 11
next sibling parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Monday, 11 January 2021 at 14:51:29 UTC, Ola Fosheim Grøstad 
wrote:
 On Monday, 11 January 2021 at 14:03:39 UTC, Paul Backus wrote:
     alias expand this;
Hm... this does not allow me protect the fields from being changed. I also cannot use const since it is transitive and would make it impossible to return two references to mutable objects? Basically, the tuple itself should be immutable, but not the objects being referenced. I guess I could run over the types and add const if they are not references?
But that would leave references mutable... so not the best solution. I kinda like the flexibility of __0, __1 and that it maps nicely to a regular struct.
Jan 11
parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Monday, 11 January 2021 at 14:53:08 UTC, Ola Fosheim Grøstad 
wrote:
 On Monday, 11 January 2021 at 14:51:29 UTC, Ola Fosheim Grøstad 
 wrote:
 On Monday, 11 January 2021 at 14:03:39 UTC, Paul Backus wrote:
     alias expand this;
Hm... this does not allow me protect the fields from being changed. I also cannot use const since it is transitive and would make it impossible to return two references to mutable objects? Basically, the tuple itself should be immutable, but not the objects being referenced. I guess I could run over the types and add const if they are not references?
But that would leave references mutable... so not the best solution. I kinda like the flexibility of __0, __1 and that it maps nicely to a regular struct.
I guess what I want is head-immutable (not even really head const).
Jan 11
prev sibling parent reply Paul Backus <snarwin gmail.com> writes:
On Monday, 11 January 2021 at 14:51:29 UTC, Ola Fosheim Grøstad 
wrote:
 Basically, the tuple itself should be immutable, but not the 
 objects being referenced. I guess I could run over the types 
 and add const if they are not references?
Why? I'd say that an `immutable(Tuple)` should be immutable, and a `Tuple` should be mutable, as is the case with literally every other type in D.
Jan 11
parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Monday, 11 January 2021 at 17:48:13 UTC, Paul Backus wrote:
 Why? I'd say that an `immutable(Tuple)` should be immutable, 
 and a `Tuple` should be mutable, as is the case with literally 
 every other type in D.
Tuples are usually immutable, it brings more correctness.
Jan 11
parent reply Paul Backus <snarwin gmail.com> writes:
On Monday, 11 January 2021 at 18:02:19 UTC, Ola Fosheim Grøstad 
wrote:
 On Monday, 11 January 2021 at 17:48:13 UTC, Paul Backus wrote:
 Why? I'd say that an `immutable(Tuple)` should be immutable, 
 and a `Tuple` should be mutable, as is the case with literally 
 every other type in D.
Tuples are usually immutable, it brings more correctness.
I agree that immutability has benefits, but I don't see why tuples should be singled out for special treatment in this regard. Why is immutability more important for tuples than for any other kind of data?
Jan 11
next sibling parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Monday, 11 January 2021 at 19:25:06 UTC, Paul Backus wrote:
 On Monday, 11 January 2021 at 18:02:19 UTC, Ola Fosheim Grøstad 
 wrote:
 On Monday, 11 January 2021 at 17:48:13 UTC, Paul Backus wrote:
 Why? I'd say that an `immutable(Tuple)` should be immutable, 
 and a `Tuple` should be mutable, as is the case with 
 literally every other type in D.
Tuples are usually immutable, it brings more correctness.
I agree that immutability has benefits, but I don't see why tuples should be singled out for special treatment in this regard. Why is immutability more important for tuples than for any other kind of data?
Well, maybe not more important, but at least consistent with mathematics. :-) I guess my real reason is that I am not ready to reimplement the type system and add one-level "readonly" semantics to the language at this point, so just preventing writable reference from leaking out seems one temporary approach that could work. What I don't want is to have a tuple accidentally modified, so if one wants to modify the tuple then one should bind it to variables through destructuring? I guess that is a good reason? The caller of a function can decide to set up the return value from a function for mutation by destructuring the tuple. I guess that would be the "idiomatic" pattern...
Jan 11
prev sibling parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Monday, 11 January 2021 at 19:25:06 UTC, Paul Backus wrote:
 I agree that immutability has benefits, but I don't see why 
 tuples should be singled out for special treatment in this 
 regard.
Oh, and another reason is that scalars can usually be passed by value with impunity, but you might want to pass tuples by reference as it could save you some copying in a significant way. And pass by reference would make the tuple vulnerable to mutation... (since we don't have head-const).
Jan 11
parent reply Paul Backus <snarwin gmail.com> writes:
On Monday, 11 January 2021 at 20:36:30 UTC, Ola Fosheim Grøstad 
wrote:
 On Monday, 11 January 2021 at 19:25:06 UTC, Paul Backus wrote:
 I agree that immutability has benefits, but I don't see why 
 tuples should be singled out for special treatment in this 
 regard.
Oh, and another reason is that scalars can usually be passed by value with impunity, but you might want to pass tuples by reference as it could save you some copying in a significant way. And pass by reference would make the tuple vulnerable to mutation... (since we don't have head-const).
All of what you're saying applies equally well to any struct type as it does to tuples. It sounds like what you really want is for D *in general* to have head-const, for all types. So there's no reason to force it on tuples in particular. Just write your DIP for 'readonly', and if it's accepted, you can write `readonly(Tuple)` and get the result you want for free.
Jan 11
parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Monday, 11 January 2021 at 20:52:25 UTC, Paul Backus wrote:
 All of what you're saying applies equally well to any struct 
 type as it does to tuples.
Sure.
 It sounds like what you really want is for D *in general* to 
 have head-const, for all types. So there's no reason to force 
 it on tuples in particular. Just write your DIP for 'readonly', 
 and if it's accepted, you can write `readonly(Tuple)` and get 
 the result you want for free.
More like head-immutable, but yes.
Jan 11
parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
Ok, so I now have this, but I think maybe the switch could be 
turned into a static array by an reinterpret cast of 
"&expand[0]"? I would assume the layout would typically be 
"expand_field_0, expand_field_1 etc...


template Tuple(Types...){
     template same(){
         static foreach (i, dummy; Types) {
             static if (i + 1 < Types.length && !is(typeof(same) 
== bool)
                        && !is(Types[i] == Types[i + 1])) {
                 enum bool same = false;
             }
         }
         static if (!is(typeof(same) == bool)) {
             enum bool same = true;
         }
     }

     struct Tuple {
         Types expand;
         alias expand this;

         static if (same!()) {
             auto opIndex(size_t i) {
                 switch (i) {
                     static foreach (j; 0 .. Types.length) {
                         case j: return this.expand[j];
                     }
                     default: assert(0);
                 }
             }
         }
     }
}
Jan 12
parent reply sighoya <sighoya gmail.com> writes:
What about this?
No magic, but I don't know the performance impact.

```
import std.meta;
import std.conv;

template same(Types...)
{
     static if (Types.length >= 2)
     {
         static if (is(Types[0] == Types[$ - 1]))
         {
             const same = same!(Types[1 .. $]);
         }
         else
         {
             enum bool same = false;
         }
     }
     else
     {
         enum bool same = true;
     }
}

struct Tuple(Types...)
{

     static if (same!Types)
     {
         public Types[0][Types.length] elements;
         public this(Types[0][Types.length] elements...)
         {
             this.elements = elements;
         }
     }

     else
     {
         static foreach (int i, T; Types)
         {
             mixin("public " ~ T.stringof ~ " " ~ "elem" ~ 
i.stringof ~ ";");
         }
         public this(Types elements)
         {
             static foreach (int i, T; Types)
             {
                 mixin("this.elem" ~ i.stringof ~ "=" ~ 
"elements[i]" ~ ";");
             }
         }
     }
}

int main()
{
     import std.stdio;

     auto homogenousTuple = Tuple!(int, int)(2, 3);
     writeln("homogenous tuple ", homogenousTuple.elements[0], ":",
             typeid(homogenousTuple.elements[0]), ":", 
homogenousTuple.elements[1],
             ":", typeid(homogenousTuple.elements[1]));
     auto heterogenousTuple = Tuple!(int, float)(2, 3);
     writeln("heterogenous tuple ", heterogenousTuple.elem0, ":", 
typeid(heterogenousTuple.elem0),
             ":", heterogenousTuple.elem1, ":", 
typeid(heterogenousTuple.elem1));
     return 0;
}
```

Problem is, the type arguments getn't inferred.
Jan 12
parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Tuesday, 12 January 2021 at 21:38:59 UTC, sighoya wrote:
 What about this?
 No magic, but I don't know the performance impact.

 ```
 import std.meta;
 import std.conv;

 template same(Types...)
 {
     static if (Types.length >= 2)
     {
         static if (is(Types[0] == Types[$ - 1]))
         {
             const same = same!(Types[1 .. $]);
         }
         else
         {
             enum bool same = false;
         }
     }
     else
     {
         enum bool same = true;
     }
 }
I also like the recursive version better. I borrowed the core concept from Phobos. I assume they don't use recursion because they are afraid of running out of stack space? Or maybe it is because of performance?
 struct Tuple(Types...)
 {

     static if (same!Types)
     {
         public Types[0][Types.length] elements;
Yes, this is what I also felt was the right approach. But then I realized that AliasSeq (which is what you get if you use "Types expand") is hardwired in the compiler in a hackish way so that you get various features from it, like implicit splatting (expanding the sequence into arguments)? That kinda makes it difficult to get the right semantics as I know of no way to do it without AliasSeq, but maybe there is some way, perhaps one can convert it into an AliasSeq somehow. I kinda don't like this kind of compiler-magic for one specific type as it limits what you can do, but I guess it was done for easy-of-implementation.
         static foreach (int i, T; Types)
         {
             mixin("public " ~ T.stringof ~ " " ~ "elem" ~ 
 i.stringof ~ ";");
         }
Yep, I had this version at some point :-) I think the way you approached it is the intuitive way, but the problem is really that AliasSeq get special treatment by the compiler so it is currently difficult to work around it? One should probably look closer at the language semantics and try to come up with a more generic mechanism as that would make for more powerful meta programming.
Jan 12