Pre-RFC: Allow existing variables in destructuring tuples

  • Existing variables in Tuple destructuring
  • 2019-09-19

Summary

This feature would allow the use of fields and existing variables to be destructed into while destructing tuples.

Motivation

There is currently no way to destructure tuples into preexisting variables (including struct fields) so do get that affect it is necessary to create new bindings and then assign again, each in turn, to the variables that you want. This works but doesn't show the intent to the best degree.

Guide-level explanation

The following code would now be allowed:

struct A {
    b: i32,
    c: i32
}

fn foo() -> (i32, i32);

fn main() {
    let a: A;

    (a.b, a.c) = foo()
}

It would be equivalent to the following:

struct A {
    b: i32,
    c: i32
}

fn foo() -> (i32, i32);

fn main() {
    let a: A;

    {
        let (foo1, foo2) = foo();
        a.b = foo1;
        a.c = foo2;
    }
}

So a destructure is able to be done without a let statement. It can be more along the line of an assignment instead of only an initialization.

Object destructuring should also be supported, for parity and simplicity of teaching.

Reference-level explanation

So a new kind of left hand side of an assignment would be allowed that doesn't have a prefixed let. This sort of left hand side would destructure a tuple into the named locations (not just variables, could be fields).

Drawbacks

  1. It makes the language more complicated
  2. Can technically do this with macros or similar

Rationale and alternatives

  • This brings the destructing up to parity in assignment along with variable defining.

Prior art

  • JS destructing assignment does allow for this sort of assignment.

Unresolved questions

  • Should mixing and matching names that have/haven't been declared yet be allowed. If they are a possible solution could be (a.b, let c) = foo() since that makes it clear which ones are being declared/assigned.
4 Likes

Why the limit to just tuple patterns, it seems like also supporting object patterns (and all other forms of patterns) as mentioned in the prior art would be more consistent

struct A {
    b: i32,
    c: i32
}

struct D {
    e: i32,
    f: i32,
}

fn foo() -> D;

fn main() {
    let a: A;

    D { e: a.c, f: a.b } = foo();
}
3 Likes

Quite true, I didn't mention that because I forgot that it existed. Will update the original post

Disclaimer: I'm not part of the relevant team, just a bystander.

The title of the pre-RFC is not entirely clear as to what the goal is. Your goal seems to be about initializing a struct via destructuring. The way you're proposing to do it uses an existing variable, but that's the how, not the what.

From the title, I'd expect things like the following to be discussed too:

struct Foo(usize);
fn foo() -> Foo;
fn main() {
    let a: usize;
    Foo(a) = foo();
}

which currently can be written as a simple let Foo(a) = foo(), but in principle, would be allowed by this proposal (at least under its current title). And if not, why not should be discussed too.

From a language perspective, at least as I understand it, what happens with patterns is that they can define bindings. Struct members are not bindings. This is a significant change for what can happen with patterns. Another significant difference is the lack of a let in that assignment.

FWIW, I think the following, which work today, are not too cumbersome as an alternative.

let a = match foo() {
    (b, c) => A { b, c }
};

or

let a = match foo() {
    D { e: c, d: b } => A { b, c }
};
2 Likes

You seem to want to introduce the production $pat = $expr for expressions, with the semantics "exhaustively match $expr against $pat, and treat each atomic pattern in $pat as a preexisting place."

This is... basically fine. Except that the lhs of = can be a complex expression, which would require embedding the expression grammar into the pattern grammar... which seems really really bad.

1 Like

In fact, I think the problematic example is super simple:

twpe::variant(a.a, a.b) = value;

This could be

Assign {
  lhs: FunctionCall {
    path: «twpe::variant»
    args: [
      Expr «a.a»
      Expr «a.b»
    ]
  }
  rhs: Expr «value»
}

or

Assign {
  lhs: Pattern::TupleType {
    path: «twpe::variant»
    fields: [
      Expr «a.a»
      Expr «a.b»
    ]
  }
  rhs: Expr «value»
}

These trivially have the same shape. You might be able to argue that it could always parse as ambiguous, as they always have the same shape between expr and pattern, but then it becomes data-dependant as to what the correct parse is: it depends on the type of twpe::variant being a type or a function. That's no good.

I've definitely wanted something like this, but this isn't a tenable way to get there.

4 Likes

This syntax seems ambiguous to me, in a pattern; it could mean assignment to an existing location, or matching equal to the value in that location. I'd like to avoid that ambiguity.

2 Likes

Could using a keyword help? Like do let so as to disambiguate the expression.

Looks like you want destructuring assignment, it was discussed previously:

2 Likes

I see, thank you for the link. I searched on here but I guess I should also have searched on the repo as well.

I guess on way of doing the assignment would be to prefix the pattern with something to disambiguate it from being things like a function call.

But you can't assign to the result of a function call... can you?

Right, I suppose that even if it returned a mut ref you'd have to deref it to get a place to which to assign.

I think that's still "grammatically correct" though which means it might mess with macros to change how it parses.

I'm having trouble thinking of an actual example that would be accepted through typeck which would be ambiguous with a pattern assign, so it could theoretically be possible to parse as "expr or pattern" then use the correct case grammatically, but I'm far from being able to show one way or another.

But then it also raises the question of patterns in other locations or requires making the "pattern in expr position" grammar distinct from other pattern positions.

Yes; I suspect that mut (a, b) = foo(); could work (since mut can't start an expression), as a strawman.

3 Likes