www.digitalmars.com         C & C++   DMDScript  

digitalmars.dip.development - First Draft: Tuple Unpacking Syntax

reply Meta <jared771 gmail.com> writes:
This DIP proposes built-in tuple unpacking syntax for D. A sample 
of the proposed syntax:
```
import std.typecons : tuple;

(int a, string b) = tuple(1, "2");
assert(a == 1);
assert(b == "2");

auto (a, b) = tuple(1, "2");
static assert(is(typeof(a) == int));
static assert(is(typeof(b) == string));

auto (a, immutable b, c) = tuple(1, "2", 3.0);
static assert(is(typeof(a) == int));
static assert(is(typeof(b) == immutable string));
static assert(is(typeof(c) == double));
```

The DIP is based on Timon Gehr's old DIP for tuple syntax in D 
(https://github.com/tgehr/DIPs/blob/tuple-syntax/DIPs/DIP1xxx-tg.md), but is
solely limited to support for unpacking; it is not a full tuple-syntax DIP. If
the reception and general sentiment for this DIP are positive, further
enhancements to add built-in tuple support to D may be proposed in the future.

Thanks to Timon and Nick Treleaven for doing the bulk of the 
implementation and conceptual work on this proposal. I mainly 
just kickstarted things and am facilitating the DIP process.

The DIP:
https://github.com/MetaLang/DIPs/blob/bf357d16b1bce65ba4ed95a08d146d4015eeb2d7/DIPs/1NNN-JH-TG-NT.md
Jul 24
next sibling parent reply "Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz> writes:
Overall quite good, just a couple of tweaks I'd prefer to have to make 
everyone's lives easier (such as custom runtime writers).



Proposal 3: Built-in tuple types and literals

Why are you putting it into object.d?

Its big enough as it is, and these types can be copied wholesale.

Putting them in a dedicated module would be a much better choice.



Proposal 6: Placeholder name _

This is guaranteed to break code, its going to have to wait for an 
edition to execute. No need to do it in two stages then.
Jul 25
parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 7/25/25 15:42, Richard (Rikki) Andrew Cattermole wrote:
 Overall quite good, just a couple of tweaks I'd prefer to have to make 
 everyone's lives easier (such as custom runtime writers).
 ...
I think you were looking at the wrong document, seems you were reading my old draft that had a bigger scope. Neither of these features are proposed by the DIP. That said:
 
 
 Proposal 3: Built-in tuple types and literals
 
 Why are you putting it into object.d?
 
 Its big enough as it is, and these types can be copied wholesale.
 
 Putting them in a dedicated module would be a much better choice.
 ...
 
Something like `core.internal.tuple` or similar will probably be better.
 
 Proposal 6: Placeholder name _
 
 This is guaranteed to break code, its going to have to wait for an 
 edition to execute. No need to do it in two stages then.
 
Yes. This is why this is not part of this DIP.
Jul 27
parent reply "Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz> writes:
You are so right, I looked at the wrong document.

In that case I'd like to point out:

1. The wording around ``ref`` and ``out`` could be improved. The 
behavior of each should be matching and it does seem to read as such, 
even if it isn't in the same paragraph.

2. Moving elements should be in DIP even if implementation doesn't 
support it. I don't think anything special needs to be done here. My 
understanding is the compiler should be seeing the VarDeclaration -> 
VarDeclaration assignment and handle it normally.
Jul 27
parent reply Nick Treleaven <nick geany.org> writes:
On Sunday, 27 July 2025 at 21:52:15 UTC, Richard (Rikki) Andrew 
Cattermole wrote:
 2. Moving elements should be in DIP even if implementation 
 doesn't support it. I don't think anything special needs to be 
 done here. My understanding is the compiler should be seeing 
 the VarDeclaration -> VarDeclaration assignment and handle it 
 normally.
I think you are asking for something special: ```d T a, b; alias seq = AliasSeq!(a, b); // auto (x, y) = a; auto x = a[0]; auto y = a[1]; ``` `a[0]` and `a[1]` are not moved by the lowered code. Do you have an example where there should be a move?
Aug 01
next sibling parent reply Nick Treleaven <nick geany.org> writes:
On Friday, 1 August 2025 at 10:08:09 UTC, Nick Treleaven wrote:
 On Sunday, 27 July 2025 at 21:52:15 UTC, Richard (Rikki) Andrew 
 Cattermole wrote:
 2. Moving elements should be in DIP even if implementation 
 doesn't support it. I don't think anything special needs to be 
 done here. My understanding is the compiler should be seeing 
 the VarDeclaration -> VarDeclaration assignment and handle it 
 normally.
I think you are asking for something special: ```d T a, b; alias seq = AliasSeq!(a, b); // auto (x, y) = a; auto x = a[0]; auto y = a[1]; ```
Sorry, wrong code above. ```d T a, b; alias seq = AliasSeq!(a, b); // auto (x, y) = seq; auto x = seq[0]; auto y = seq[1]; ```
 `a[0]` and `a[1]` are not moved by the lowered code.

 Do you have an example where there should be a move?
Aug 01
parent Nick Treleaven <nick geany.org> writes:
On Friday, 1 August 2025 at 10:36:37 UTC, Nick Treleaven wrote:
 On Friday, 1 August 2025 at 10:08:09 UTC, Nick Treleaven wrote:
 Sorry, wrong code above.
 ```d
     T a, b;
     alias seq = AliasSeq!(a, b);
     // auto (x, y) = seq;
     auto x = seq[0];
     auto y = seq[1];
 ```

 `a[0]` and `a[1]` are not moved by the lowered code.
So obviously `a` and `b` are lvalues and shouldn't be moved, sorry. Other cases: ```d import std; struct S { int i; this(ref S s) { writeln("copy", s.i); } } void main() { writeln("rvalue"); auto a = tuple(S(1))[0]; // copy, not move! writeln("lvalue"); auto t = tuple(S(2)); writeln("index"); auto b = t[0]; // copy, OK writeln("seq"); auto c = AliasSeq!(S(3))[0]; // move, OK } ``` So the DIP should move when unpacking into `c`. For `a`, I think we would need tuple literals to avoid a copy.
Aug 02
prev sibling parent reply "Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz> writes:
On 01/08/2025 10:08 PM, Nick Treleaven wrote:
 On Sunday, 27 July 2025 at 21:52:15 UTC, Richard (Rikki) Andrew 
 Cattermole wrote:
 2. Moving elements should be in DIP even if implementation doesn't 
 support it. I don't think anything special needs to be done here. My 
 understanding is the compiler should be seeing the VarDeclaration -> 
 VarDeclaration assignment and handle it normally.
I think you are asking for something special: ```d     T a, b;     alias seq = AliasSeq!(a, b);     // auto (x, y) = a;     auto x = a[0];     auto y = a[1]; ``` `a[0]` and `a[1]` are not moved by the lowered code. Do you have an example where there should be a move?
You are getting to what I was thinking, there is no reason to mention copying or moving. Its defined behavior elsewhere.
Aug 01
parent Timon Gehr <timon.gehr gmx.ch> writes:
On 8/2/25 00:06, Richard (Rikki) Andrew Cattermole wrote:
 On 01/08/2025 10:08 PM, Nick Treleaven wrote:
 On Sunday, 27 July 2025 at 21:52:15 UTC, Richard (Rikki) Andrew 
 Cattermole wrote:
 2. Moving elements should be in DIP even if implementation doesn't 
 support it. I don't think anything special needs to be done here. My 
 understanding is the compiler should be seeing the VarDeclaration -> 
 VarDeclaration assignment and handle it normally.
I think you are asking for something special: ```d      T a, b;      alias seq = AliasSeq!(a, b);      // auto (x, y) = a;      auto x = a[0];      auto y = a[1]; ``` `a[0]` and `a[1]` are not moved by the lowered code. Do you have an example where there should be a move?
You are getting to what I was thinking, there is no reason to mention copying or moving. Its defined behavior elsewhere.
The _limitations_ section mentions moves because there is no move-unpack. `auto (x, y) = move(a);` moves `a` into a temporary, copies `a[0]` and `a[1]`, then destroys the temporary: ```d import std.stdio; import core.lifetime; struct S{ this(this){ writeln("S copied"); } ~this(){ writeln("S destroyed"); } } struct Tuple(T...) { T expand; alias expand this; this(this){ writeln("Tuple copied"); } ~this(){ writeln("Tuple destroyed"); } } void main(){ Tuple!(S, S) t; auto (a, b) = move(t); } ``` ``` $ dmd -run test.d S copied S copied S destroyed S destroyed Tuple destroyed S destroyed S destroyed Tuple destroyed S destroyed S destroyed ``` I.e, it copies the two fields into a temporary, then it destroys the temporary. What it would look like without the limitation: ``` $ dmd -run test.d S destroyed S destroyed Tuple destroyed S destroyed S destroyed Tuple destroyed S destroyed S destroyed ``` The pie-in-the-sky ideal output: ``` S destroyed S destroyed ``` I also noticed that this causes an ICE in the DMD backend atm, will have to fix it before upstreaming the implementation: ```d import std.stdio; import core.lifetime; struct S{ this(ref S){ writeln("S copied"); } this(S){ writeln("S moved"); } ~this(){ writeln("S destroyed"); } } struct Tuple(T...) { T expand; alias expand this; this(ref Tuple rhs){ this.expand=rhs.expand; writeln("Tuple copied"); } this(Tuple rhs){ static foreach(i;0..expand.length) this.expand[i]=move(rhs.expand[i]); writeln("S moved"); } ~this(){ writeln("Tuple destroyed"); } } void main(){ Tuple!(S, S) t; auto (a, b) = move(t); } ``` Once this works though it will still have copies in it, as you can verify by annotating the copy constructor of `S` with ` disable`.
Aug 20
prev sibling next sibling parent reply Walter Bright <newshound2 digitalmars.com> writes:
This is a well written DIP. Congratulations!

The forward range tuple is pretty cool.

Let's move forward with this.

bearophile's example uses `_` as a placeholder, but I don't see it mentioned in 
the rest of the DIP?

For the example:
```
auto arr = [tuple(1, "2"), tuple(3, "4"), tuple(5, "6")];

foreach((x, y); arr) {
     writeln(x, " ", y); // "1 2\n3 4\n5 6"
}

foreach((int x, string y); arr) {
     writeln(x, " ", y);// "1 2\n3 4\n5 6"
}
```

shouldn't there be a trailing \n after the 6?
Jul 26
parent Timon Gehr <timon.gehr gmx.ch> writes:
On 7/27/25 08:31, Walter Bright wrote:
 This is a well written DIP. Congratulations!
 
 The forward range tuple is pretty cool.
 
 Let's move forward with this.
 
 bearophile's example uses `_` as a placeholder, but I don't see it 
 mentioned in the rest of the DIP?
 ...
He had used it as a placeholder by convention, but it's just a valid identifier. It would not be possible to use it for two distinct variables in the same scope. I.e., you can do: ```d int _ = 2; ``` But not: ```d int _ = 3; int _ = 4; ``` Which would work with a true placeholder.
 For the example:
 ```
 auto arr = [tuple(1, "2"), tuple(3, "4"), tuple(5, "6")];
 
 foreach((x, y); arr) {
      writeln(x, " ", y); // "1 2\n3 4\n5 6"
 }
 
 foreach((int x, string y); arr) {
      writeln(x, " ", y);// "1 2\n3 4\n5 6"
 }
 ```
 
 shouldn't there be a trailing \n after the 6?
Yes.
Jul 27
prev sibling parent reply IchorDev <zxinsworld gmail.com> writes:
On Friday, 25 July 2025 at 04:27:53 UTC, Meta wrote:
 The DIP:
 https://github.com/MetaLang/DIPs/blob/bf357d16b1bce65ba4ed95a08d146d4015eeb2d7/DIPs/1NNN-JH-TG-NT.md
 [...]
 Moving elements (rather than copying) from the value sequence 
 is not supported.
Could you please add an explanation for this? I really don't understand why it wouldn't be supported, and I would certainly prefer for unpacking to be a move rather than a copy in general.

 Unpacking a tuple into lvalues is postponed until tuple 
 literals are supported, because both redefine the comma 
 operator syntax.
 ```d
 auto t = (1, 2); // tuple literal
 int x, y;
 (x, y) = t;
 assert(x == 1);
 assert(y == 2);
 ```
You can pretty-much already do this like so: ```d int x, y; AliasSeq!(x, y) = t; ```
Jul 31
parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 7/31/25 13:46, IchorDev wrote:
 I really don't understand why it wouldn't be supported
Because it is not implemented and there is not yet a spec for it. Also, move semantics is still somewhat in flux. I guess one way to do it for tuples would be to move each field on its own if the unpack rhs is an rvalue. Not sure how to implement that, but I can try during the dconf hackathon. I guess would need to call the move constructor and then blit `.init` on the source field. Ideally though it would not even run the destructor on the moved fields anymore. Eliding all the destructor calls would need some cooperation from the tuple type though. I had hoped there would be some way to move on last use and avoid any further destructor calls, but it's not really where we have been moving so far and therefore there has been no progress on interations of unpacking with moves either.
Aug 20
parent IchorDev <zxinsworld gmail.com> writes:
On Wednesday, 20 August 2025 at 16:11:42 UTC, Timon Gehr wrote:
 On 7/31/25 13:46, IchorDev wrote:
 I really don't understand why it wouldn't be supported
Because it is not implemented and there is not yet a spec for it.
That's the case of the whole DIP. It sounds like they're proposing tuple unpacking whereby it always requires a copy; which would defeat some of the point of having functions return tuples in the future, because it'd be no better (from a code optimisation standpoint) than just placing the return values in a struct & unpacking manually.
 I guess one way to do it for tuples would be to move each field 
 on its own if the unpack rhs is an rvalue. Not sure how to 
 implement that, but I can try during the dconf hackathon. I 
 guess would need to call the move constructor and then blit 
 `.init` on the source field.

 Ideally though it would not even run the destructor on the 
 moved fields anymore. Eliding all the destructor calls would 
 need some cooperation from the tuple type though.
Yes. That sounds amazing. Please, please do all of that.
 I had hoped there would be some way to move on last use and 
 avoid any further destructor calls, but it's not really where 
 we have been moving so far
Alas...
Aug 23