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

Open
glaebhoerl opened this Issue Oct 8, 2014 · 108 comments

Comments

Projects
None yet
@glaebhoerl
Copy link
Contributor

glaebhoerl commented Oct 8, 2014

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

This comment has been minimized.

Copy link

sbditto85 commented Dec 30, 2014

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

@MattWindsor91

This comment has been minimized.

Copy link

MattWindsor91 commented Dec 30, 2014

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

This comment has been minimized.

Copy link

arthurprs commented Jan 14, 2015

EDIT: not so fond of it anymore

@bstrie

This comment has been minimized.

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

This comment has been minimized.

Copy link
Contributor Author

glaebhoerl commented Jan 14, 2015

@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

This comment has been minimized.

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

This comment has been minimized.

Copy link

DavidJFelix commented Jan 19, 2015

👍 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

This comment has been minimized.

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

This comment has been minimized.

Copy link

bombless commented Jan 21, 2015

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

This comment has been minimized.

Copy link

DavidJFelix commented Jan 21, 2015

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

@bombless

This comment has been minimized.

Copy link

bombless commented Jan 21, 2015

@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

This comment has been minimized.

Copy link

DavidJFelix commented Jan 21, 2015

@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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Member

carllerche commented Mar 9, 2015

I am 👍 for this too

@sharpjs

This comment has been minimized.

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

This comment has been minimized.

Copy link

yongqli commented Dec 11, 2015

👍 I would love to see this implemented.

@Manishearth

This comment has been minimized.

Copy link
Member

Manishearth commented Jan 26, 2016

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Copy link

arthurprs commented Jan 27, 2016

@yongqli that's very interesting, thanks for sharing

@flying-sheep

This comment has been minimized.

Copy link

flying-sheep commented Feb 17, 2016

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

This comment has been minimized.

Copy link

KalitaAlexey commented Feb 17, 2016

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

@flying-sheep

This comment has been minimized.

Copy link

flying-sheep commented Feb 17, 2016

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

@KalitaAlexey

This comment has been minimized.

Copy link

KalitaAlexey commented Feb 17, 2016

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

This comment has been minimized.

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

This comment has been minimized.

Copy link

flying-sheep commented Feb 17, 2016

yeah, the &mut thing doesn’t work:

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

@ldpl

This comment has been minimized.

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

This comment has been minimized.

Copy link

KalitaAlexey commented Feb 17, 2016

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

This comment has been minimized.

Copy link
Contributor

ticki commented Feb 17, 2016

@KalitaAlexey, you can already destructure with let.

@suhr suhr referenced this issue Mar 10, 2017

Open

Language ergonomic/learnability improvements #17

10 of 31 tasks complete
@torpak

This comment has been minimized.

Copy link

torpak commented Aug 2, 2017

What about using something like tie from c++?

let (a, b) = (3, 4);
...
tie (a, b) = (5, 6);

that is just as easy to write and needs no complex extension of the parser.

@eddyb

This comment has been minimized.

Copy link
Member

eddyb commented Aug 2, 2017

@torpak I thought the parser problem is a solved one: don't try to have full pattern syntax oh the LHS, handle oh the intersection with expressions.
Since tie is not already a keyword, what you wrote parses. In fact...
tie(a, b).x = (5, 6); could even pass all checks and run.
The issue here from what I can tell is people don't seem to like the "intersection of expressions and patterns" approach even when it could work well for most cases.

@louy2

This comment has been minimized.

Copy link

louy2 commented Sep 15, 2017

Is there a properly formatted RFC for this feature yet?

@steveklabnik

This comment has been minimized.

Copy link
Member

steveklabnik commented Sep 15, 2017

I am not aware of any.

@c0b

This comment has been minimized.

Copy link

c0b commented Oct 10, 2017

so it's only because no RFC for this yet? and no committers have reviewed ? can we call a few of them by mentioning

coming from Python world, this is very natural to calculate Fibonacci numbers, by tuple assignment:

In [1]: a, b = 1, 1

In [2]: for i in range(10):
   ...:     a, b = b, a+b
   ...:     

In [3]: a, b
Out[3]: (89, 144)

and Javascript ES7 does not have tuple, but have object construct and destruct:

> let a = 1, b = 1;
undefined
> for (let i in Array.from({length: 10})) {
... ({ a, b } = { a: b, b: a+b });
... }
{ a: 89, b: 144 }

and Javascript Array spread assignment, this is valid since ES6 (ES2015)

> for (let i = 0; i < 10; i++) [ a, b ] = [ b, a+b ];
[ 89, 144 ]
@burdges

This comment has been minimized.

Copy link

burdges commented Oct 10, 2017

I dislike that this encourages unneeded mutability.

Instead of new assignment syntax for mutable variables, what about some assign/mutate/unlet syntax that locally altered the behavior of patterns from declaration to mutating assignment?

let mut field2;
let x = foo_mut();
loop {
   ...
   let Struct {
       field0,  mut field1,  // ordinary let declarations using destructuring with field puns
       field3: assign!(*x) // assignment to mutate the referent of x
       assign!(field3),   // assignment to existing mutable variable field3 ala field puns
   } = rhs();
   ...
}

It'd be obnoxious to write (assign!(a), assign!(b), assign!(c)) = rhs(); of course, but you should never do that anyways since invariably some elements should always be new immutable declarations instead of assignments to mutable variables. Also, this approach might work inside match or while/if let, and in more complex destructurings.

I picked a macro syntax here because it's only sugar declaring another binding and assigning to the mutable variable. Also, the () help delineate a full lhs term when dereferencing. I think unlet!(), mutate!(), or even mut!() could all make reasonable names as well. Alternatively new names like assign, unlet, mutate could work as keywords too, ala (a, unlet b, c) = rhs();. I could imagine sigil based syntaxes or even using =, ala (a, (b=_), c) = rhs();, although field puns might be problematic.

@MichaelBell

This comment has been minimized.

Copy link

MichaelBell commented Dec 3, 2017

As a rust newbie I'd just like to add my +1 that it's definitely confusing that you can do
let (a, b) = fn_that_returns_tuple();
but not
(a, b) = fn_that_returns_tuple();

I understand there are issues with implementation, but @dgrunwald seems to be saying there's a simple option that would work in the majority of cases that people actually care about - I think that would be worth doing!

@louy2

This comment has been minimized.

Copy link

louy2 commented Dec 19, 2017

@burdges
In pure languages this is unnecessary at least partly because loops like @tstorch's can (or must) be easily turned into recursive helper functions. But in Rust, with tail call optimization not guaranteed, as well as its goal of attracting audience from dynamic languages, I think destructuring assignment is a reasonable compromise.

@phaux

This comment has been minimized.

Copy link

phaux commented Dec 19, 2017

Would it be possible to allow tuples/structs as actual lvalues, so that we don't need to differentiate between pattern/expr at all?

@eddyb

This comment has been minimized.

Copy link
Member

eddyb commented Dec 19, 2017

@phaux I would hope so (syntactically), preferably in a way that (*x, y.field) = (a, b) works.

@Kimundi

This comment has been minimized.

Copy link
Member

Kimundi commented Dec 19, 2017

Hm, now that's an idea, though I'm not sure how backwards compatible it would be:

let a = 5;
let b = "hello".to_string();

let (ref x, ref y) = (a, b); // this would move `a` and `b` today, but just reference them with constructors becoming lvalues.

That said, I think it wouldn't work anyway, as you would need a single canonical address for a constructor lvalue - which means constructing it from other lvalues would not really work (unless we special case it in the language, such that you get only an error if you attempt to take the address of a constructor lvalue, or magically let it behave as a rvalue in that case)

@eddyb

This comment has been minimized.

Copy link
Member

eddyb commented Dec 19, 2017

@Kimundi I don't think it's plausible to have something like that, no, I interpreted @phaux's to refer to the alternative I like which is keep parsing expr = expr but interpret the LHS differently than the RHS, without involving patterns or creating some sort of "value" for the LHS.

@phaux

This comment has been minimized.

Copy link

phaux commented Dec 19, 2017

@eddyb Exactly.

@Kimundi

This comment has been minimized.

Copy link
Member

Kimundi commented Dec 19, 2017

Ah, then I misunderstood. So basically one of the things which have been proposed already.

@burdges

This comment has been minimized.

Copy link

burdges commented Dec 19, 2017

If Foo is Sized and big, then the API will handle fn foo() -> Foo by creating an uninitialized Foo passing foo the pointer to it, yes? So Foo { whatever } = foo(); requires creating a temporary, right? You presumably meant that semantically, but not sure I understand that either.

I'm still nervous about encouraging mutability, but could the syntactic issue be handled by "commuting" the let inside the "pattern"? So

let a;  // uninitialized here
let mut b = bar();  // mutable
let c = baz() : &mut u64;  // mutable reference
virtual Foo { a, b, *c, let d, let mut e } = foo(); // named field puns
// only a and d are immutable 

You could replace virtual with another keyword, or remove it entirely, except people worried about the grammar complexity up thread. You could not however do let r = virtual Foo { a, b, *c, let d, let mut e }; r = foo(); because r would be only partially initialized, which might be @eddyb's point.

@eddyb

This comment has been minimized.

Copy link
Member

eddyb commented Dec 19, 2017

@burdges Nobody is encouraging mutability, just bringing the cases where you need to mutate closer to those where you can just bind the resulting value. As for temporaries, let has them too.

Also, I'd expect this to "just work" (to initialize the two new variables):

let (d, mut e);
Foo { a, b, *c, d, e } = foo();

And I have no idea what you mean by that virtual Foo {...} syntax on the RHS of let.

@alexreg

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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 label Feb 14, 2018

@noelzubin

This comment has been minimized.

Copy link

noelzubin commented Sep 25, 2018

any update on this ?

@varkor

This comment has been minimized.

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.

@Walther

This comment has been minimized.

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

This comment has been minimized.

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

Merge pull request rust-lang#372 from runspired/ember-data-custom-rec…
…ords

[RFC ember-data] modelFactoryFor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.