Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Destructuring assignment #372

Closed
glaebhoerl opened this issue Oct 8, 2014 · 112 comments · Fixed by #2909
Closed

Destructuring assignment #372

glaebhoerl opened this issue Oct 8, 2014 · 112 comments · Fixed by #2909
Labels
A-expressions Term language related proposals & ideas A-syntax Syntax related proposals & ideas T-lang Relevant to the language team, which will review and decide on the RFC.

Comments

@glaebhoerl
Copy link
Contributor

Given

struct Point { x: int, y: int }
fn returns_point(...) -> Point { ... }
fn returns_tuple(...) -> (int, int) { ... }

it would be nice to be able to do things like

let a; let b;
(a, b) = returns_tuple(...);
let c; let d;
Point { x: c, y: d } = returns_point(...);

and not just in lets, as we currently allow.

Perhaps even:

let my_point: Point;
(my_point.x, my_point.y) = returns_tuple(...);

(Most use cases would likely involve mut variables; but those examples would be longer.)

Related issues from the rust repo: rust-lang/rust#10174 rust-lang/rust#12138

@sbditto85
Copy link

Not sure the best way to indicate a vote in the affirmative, but 👍 +1 for this

@MattWindsor91
Copy link

Not sure how rust's RFC process goes, but I assume this needs to be written up in the appropriate RFC format first? I like it, mind.

@arthurprs
Copy link

EDIT: not so fond of it anymore

@bstrie
Copy link
Contributor

bstrie commented Jan 14, 2015

@glaebhoerl, how do you expect this to be done? It seems to me that it would require the ability for patterns to appear in arbitrary positions, which strikes me as completely infeasible.

@glaebhoerl
Copy link
Contributor Author

@bstrie I don't have any plans myself. There was some discussion of this elsewhere, possibly on the rust repository issues - I think the idea might've been that we could take the intersection of the pattern and expression grammars?

@bstrie
Copy link
Contributor

bstrie commented Jan 14, 2015

Assuming that we took the easy route and made this apply only to assignments, we'd also need to take our grammar from LL(k) to LL(infinity). I also don't think that an arbitrarily restricted pattern grammar will make the language easier to read and understand. Finally, the only time when this feature would be useful is when you can't use a new let binding because of scope, in which case the current workaround is to use a temporary. I'm not currently convinced that the gain is worth the cost.

@DavidJFelix
Copy link

👍 I've found myself wanting this from time to time, especially in reducing repetition in match statements or normal assignment. Right now I'm using small purpose-built functions instead of this. I haven't considered if it would be possible to abuse a feature like this easily or not.

@tstorch
Copy link

tstorch commented Jan 21, 2015

I would be thrilled if this would be implemented! Here is a small example why:

Currently in libcore/str/mod.rs the function maximal_suffix looks like this:

fn maximal_suffix(arr: &[u8], reversed: bool) -> (uint, uint) {
    let mut left = -1; // Corresponds to i in the paper
    let mut right = 0; // Corresponds to j in the paper
    let mut offset = 1; // Corresponds to k in the paper
    let mut period = 1; // Corresponds to p in the paper

    while right + offset < arr.len() {
        let a;
        let b;
        if reversed {
            a = arr[left + offset];
            b = arr[right + offset];
        } else {
            a = arr[right + offset];
            b = arr[left + offset];
        }
        if a < b {
            // Suffix is smaller, period is entire prefix so far.
            right += offset;
            offset = 1;
            period = right - left;
        } else if a == b {
            // Advance through repetition of the current period.
            if offset == period {
                right += offset;
                offset = 1;
            } else {
                offset += 1;
            }
        } else {
            // Suffix is larger, start over from current location.
            left = right;
            right += 1;
            offset = 1;
            period = 1;
        }
    }
    (left + 1, period)
}

This could easily look like this:

fn maximal_suffix(arr: &[u8], reversed: bool) -> (uint, uint) {
    let mut left = -1; // Corresponds to i in the paper
    let mut right = 0; // Corresponds to j in the paper
    let mut offset = 1; // Corresponds to k in the paper
    let mut period = 1; // Corresponds to p in the paper

    while right + offset < arr.len() {
        let a;
        let b;
        if reversed {
            a = arr[left + offset];
            b = arr[right + offset];
        } else {
            a = arr[right + offset];
            b = arr[left + offset];
        };
        // Here is the interesting part
        (left, right, offset, period) =
            if a < b {
                // Suffix is smaller, period is entire prefix so far.
                (left, right + offset, 1, right - left)
            } else if a == b {
                // Advance through repetition of the current period.
                if offset == period {
                    (left, right + offset, 1, period)
                } else {
                    (left, right, offset + 1, period)
                }
            } else {
                // Suffix is larger, start over from current location.
                (right, right + 1, 1, 1)
            };
        // end intereseting part
    }
    (left + 1, period)
}

If we apply, what is currently possible this would be the result:

fn maximal_suffix(arr: &[u8], reversed: bool) -> (uint, uint) {
    // Corresponds to (i, j, k, p) in the paper
    let (mut left, mut right, mut offset, mut period) = (-1, 0, 1, 1);

    while right + offset < arr.len() {
        let (a, b) =
            if reversed {
                (arr[left + offset], arr[right + offset])
            } else {
                (arr[right + offset], arr[left + offset])
            };
        (left, right, offset, period) =
            if a < b {
                // Suffix is smaller, period is entire prefix so far.
                (left, right + offset, 1, right - left)
            } else if a == b {
                // Advance through repetition of the current period.
                if offset == period {
                    (left, right + offset, 1, period)
                } else {
                    (left, right, offset + 1, period)
                }
            } else {
                // Suffix is larger, start over from current location.
                (right, right + 1, 1, 1)
            };
    }
    (left + 1, period)
}

This is easily more readble and I guess readbility of code is a major contribution to code safety and attracts more people to the language and projects written in that laguage.

@bombless
Copy link

It doesn't feel right...
If you insist, I think this looks better:

introduce a, b;
let (a, b) = returns_tuple(...);
introduce c, d;
let Point { x: c, y: d } = returns_point(...);

Still doesn't feel right, but looks more reasonable.

@DavidJFelix
Copy link

So already @bombless this clashes for me as introduce would then become the longest word in rust.

@bombless
Copy link

@DavidJFelix I don't know, I'd say -1 for this assignment idea.
And maybe change introduce to intro will make you feel better.

@DavidJFelix
Copy link

@bombless, a bit but not much. The point of "let" isn't to offer assignment, it's to introduce the variable. Assignment is done with an assignment operator, "=", If we use both the "=" and let for assignment, it becomes redundant. This is why you see:

let mut x: uint;
...
x = 123456789;

the point of this issue is that "let" allows us to unravel tuple-packed variables as we declare them and also set their value in one assignment, rather than multiple assignments; but later throughout the program, the assignment operator ceases to do this unraveling and must be done for each variable.

@taralx
Copy link

taralx commented Feb 18, 2015

So there's two ways to do this. With a desugaring pass (easier) or by actually extending the implementation of ExprAssign in the typechecker and translation. The former works, but I suspect it doesn't produce as nice a set of error messages when types don't match.

Thoughts?

@carllerche
Copy link
Member

I am 👍 for this too

@sharpjs
Copy link

sharpjs commented Oct 13, 2015

👍 Ran into this today. I'm surprised that it's not implemented already. A function can return a tuple. If I can bind that tuple via a destructuring let, it's perfectly reasonable also to assign that tuple to some bindings I already have.

let (mut kind, mut ch) = input.classify();
// ... later ...
(kind, ch) = another_input.classify();

@yongqli
Copy link

yongqli commented Dec 11, 2015

👍 I would love to see this implemented.

@Manishearth
Copy link
Member

Note that this means that in the grammar an assignment statement can take both an expression and a pattern on the lhs. I'm not too fond of that.

@taralx
Copy link

taralx commented Jan 26, 2016

It's not just any expression -- only expressions that result in lvalues, which is probably unifiable with the irrefutable pattern grammar.

@yongqli
Copy link

yongqli commented Jan 27, 2016

In the future this could also prevent excessive mem::replaces.

For example, right now I have code like:

let (xs, ys) = f(mem::replace(&mut self.xs, vec![]), mem::replace(&mut self.ys, vec![]));
self.xs = xs;
self.ys = ys;

If the compiler understood the concept of a "multi-assignment", in the future this might be written as:

(self.xs, self.ys) = f(self.xs, self.ys);

Edit: Now, of course, we can re-write f to take &muts instead. However, the semantics are a little bit different and won't always be applicable.

@arthurprs
Copy link

@yongqli that's very interesting, thanks for sharing

@flying-sheep
Copy link

does this cover AddAssign and friends? would be cool to do:

let (mut total, mut skipped) = (0, 0);
for part in parts {
    (total, skipped) += process_part(part);
}

@KalitaAlexey
Copy link

@flying-sheep You would make this when #953 will landed.

@flying-sheep
Copy link

it’s already accepted, so what’s the harm in including a section about it in this RFC now?

@KalitaAlexey
Copy link

I mean you can do

for part in parts {
    (total, skipped) += process_part(part);
}

Edit: You cannot. Because (total, skipped) creates a tuple. To change previous defined variable you should write

for part in parts {
    (&mut total, &mut skipped) += process_part(part);
}

@ticki
Copy link
Contributor

ticki commented Feb 17, 2016

This is impossible with context-free grammars. In context sensitive grammars, it is entirely possible. It seems that after the ? RFC was accepted, the parser will introduce a context-sensitive keyword, catch (since it is not reserved). This makes the Rust grammar partially context sensitive (i.e. conditional context scanning). But there is one problem with doing that here: an assignment can appear in any arbitrary (with a few exceptions) position, making partial context scanning this very hard.

I doubt it is possible without making the parser full-blown context sensitive. I could be wrong, though.

@flying-sheep
Copy link

yeah, the &mut thing doesn’t work:

binary assignment operation += cannot be applied to type (&mut _, &mut _)

@ldpl
Copy link

ldpl commented Feb 17, 2016

How about adding or reusing a keyword to avoid context-sensitive grammar? For example, "mut" seems to fit well (also reflects let syntax):

let a; let b;
mut (a, b) = returns_tuple(...);
let c;
mut Point {x: c, .. } = returns_point(...);
let Point {y: d, .. } = returns_point(...);

@KalitaAlexey
Copy link

I don't like it.

I like

let (mut a, mut b) = get_tuple();

let SomeStruct(mut value) = get_some_struct();

let Point {x: mut x, .. } = get_point();

I don't like

let mut a;
let mut b;
(a, b) = get_tuple();

I don't like

let my_point: Point;
(my_point.x, my_point.y) = returns_tuple(...);

I'd like to write

let (x, y) = returns_tuple(...);
let my_point = Point {x: x, y: y};

I just think that code must be easy readable.

@ticki
Copy link
Contributor

ticki commented Feb 17, 2016

@KalitaAlexey, you can already destructure with let.

@alexreg
Copy link

alexreg commented Dec 20, 2017

Is anything preventing this moving to RFC stage, or has no one simply taken up the task of writing one yet?

@apiraino
Copy link

apiraino commented Feb 14, 2018

Hello,

I didn't follow the whole discussion, but I agree with @MichaelBell this is a bit confusing (me newbie too). It took me a good deal of time before arriving here and finally understand what was happening.

In this test case the situation is simpler and the compiler (as of Feb, 2018) warns me about "variables not needing to be mutable", that did sound strange to me.

@burdges
Copy link

burdges commented Feb 14, 2018

I seemingly missed that comment. I meant (a,b) = foo(); should not initialize anything new, only mutate existing values, but (let a, b) = foo(); would initialize a provided b was mutable, and (let a, let b) = foo(); would equivalent to let (a,b) = foo();

In essence, let a provides an lvalue wherever it appears inside a mutating assignment pattern. I wrote virtual only because some previous comments worried about indicating the presence of a mutating assignment pattern. Initialization patterns like in match would work exactly like they do now.

@Centril Centril added the T-lang Relevant to the language team, which will review and decide on the RFC. label Feb 14, 2018
@noelzubin
Copy link

any update on this ?

@varkor
Copy link
Member

varkor commented Sep 25, 2018

If someone wanted to write an RFC taking the points in this thread into account, I'm sure it'd be well-received.

@Centril Centril added A-syntax Syntax related proposals & ideas A-expressions Term language related proposals & ideas labels Nov 27, 2018
@Walther
Copy link

Walther commented Feb 27, 2019

Wrote an initial draft of the RFC here based on this thread.

Comments would be super helpful, thank you!

@alexreg
Copy link

alexreg commented Feb 27, 2019

Thanks @Walther, that's super.

wycats pushed a commit to wycats/rust-rfcs that referenced this issue Mar 5, 2019
Centril added a commit to Centril/rust that referenced this issue Dec 22, 2019
…ntril

Improve diagnostics for invalid assignment

- Improve wording and span information for invalid assignment diagnostics.
- Link to rust-lang/rfcs#372 when it appears the user is trying a destructuring assignment.
- Make the equality constraint in `where` clauses error consistent with the invalid assignment error.
Centril added a commit to Centril/rust that referenced this issue Dec 23, 2019
…ntril

Improve diagnostics for invalid assignment

- Improve wording and span information for invalid assignment diagnostics.
- Link to rust-lang/rfcs#372 when it appears the user is trying a destructuring assignment.
- Make the equality constraint in `where` clauses error consistent with the invalid assignment error.
@varkor
Copy link
Member

varkor commented Jan 2, 2020

@fanzier, @cartesiancat and I are going to tackle this feature. We're going to start with an implementation, so we can establish the feasibility, and then follow up with a formal RFC once everything appears to be working.

@Boscop
Copy link

Boscop commented Mar 6, 2020

@varkor Thanks, I'm looking forward to it, I often wished this was already possible.

@varkor
Copy link
Member

varkor commented Apr 17, 2020

@fanzier and I have opened an RFC for destructuring assignment. We also have a working prototype at rust-lang/rust#71156.

@efikarl
Copy link

efikarl commented Dec 4, 2020

I'm looking forward to this feature, too.

For my question:

use std::net::{ UdpSocket };


fn main() -> std::io::Result<()> {
    let mut buf: Vec<u8> = Vec::new();
    let socket = UdpSocket::bind("0.0.0.0:8080")?;
    let (_len, src) = socket.recv_from(&mut buf)?;
    loop {
        // A resp Packet::Data to B
        socket.send_to(&buf, src)?;
        // B send Packet::Ack to A
        let (_len, src) = socket.recv_from(&mut buf)?;
    }
}

There will be a error for: 'src' inside the loop: note: #[warn(unused_variables)] on by default

But if we can destructuring assignment:

use std::net::{ UdpSocket, SocketAddr};


fn main() -> std::io::Result<()> {
    let mut buf: Vec<u8> = Vec::new();
    let socket = UdpSocket::bind("0.0.0.0:8080")?;
    let (mut len, mut src): (usize, SocketAddr) = socket.recv_from(&mut buf)?;
    loop {
        // A resp Packet::Data to B
        socket.send_to(&buf, src)?;
        // B send Packet::Ack to A
        (len, src) = socket.recv_from(&mut buf)?;
    }
}

Instead of that as following, something seems verbose:

fn main() -> std::io::Result<()> {
    let mut buf: Vec<u8> = Vec::new();
    let socket = UdpSocket::bind("0.0.0.0:8080")?;
    let (mut _len, mut src): (usize, SocketAddr) = socket.recv_from(&mut buf)?;
    loop {
        // A resp Packet::Data to B
        socket.send_to(&buf, src)?;
        // B send Packet::Ack to A
        let (_len, isrc) = socket.recv_from(&mut buf)?;
        src = isrc;
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-expressions Term language related proposals & ideas A-syntax Syntax related proposals & ideas T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging a pull request may close this issue.