Pre-RFC: variadic tuple

This sounds nonsensical to me. Variables should always have only one value. That's why I thought that destructuring (..v) returns a variadic tuple again, and not some number of values that can only be used within an expansion form. You didn't make this sufficiently clear in the RFC, and even if you did, programmers would probably find this very confusing.

Why not allow writing

impl<(..(K, V))> MegaMap<(..(K, V))>
    where ..(K: Hash),
{
    fn get(&self, k: (..K)) -> (..Option<V>) {
        (for (ref k, map) repeat in ..(k, self.maps) {
            HashMap::<K, V>::get(&map, k)
        })
    }
}

? The expansion form could expand a variadic tuple like this (assuming K and V are (i32, i64) and (String, String)):

    fn get(&self, k: (i32, i64)) -> (Option<String>, Option<String>) {
        (
            { HashMap::<i32, String>::get(&map, k.0) },
            { HashMap::<i64, String>::get(&map, k.1) },
        )
    }

This is more elegant, it's less restrictive and easier to understand.

Then it would be legal to put them into tuples, but it's not necessesary.

  • <A, ..B, C> would expand to <A, B1, B2, ..., Bn, C>
  • <A, (..B), C> expands to <A, (B1, B2, ..., Bn), C>.
  • <..(A, B)> would expand to <(A1, B1), (A2, B2), ..., (An, Bn)>.

As I already said, I propose to allow omitting the outer parentheses only in generic blocks, such as impl<...>, fn foo<...>, struct Foo<...>, enum Foo<...>, and in where clauses.

That's completely makes sense, so the current state is an issue.

We can then directly use a tuple identifier in the for loop, although, it misses the K and V type variable declarations. I think if you need to use it, it has to be declared. But in that case, the compiler can deduce from the arguments:

impl<(..(K, V))> MegaMap<(..(K, V))>
    where ..(K: Hash),
{
    fn get(&self, k: (..K)) -> (..Option<V>) {
        (for (ref k, map) in ..(k, self.maps) {
             map.get(k)
        })
    }
}

Also the syntax makes sense for any tuple types, not only variadic tuples. (But this is probably not useful as in that case the user unroll the loop by hand).

I'll update the RFC in the sense that a variadic tuple is a tuple and not it's member list.

I am ok with those, but as long as more than one variadic tuple is involved, I think that the enclosing parenthesis are mandatory. I feel that it will be more difficult to either read as human or implement in the compiler if this is not enforced.

In most situations it wouldn't matter, because the types can be inferred. Example:

struct Foo<..(S: ToString), ..(P: Pattern)>

impl<..(S: ToString), ..(P: Pattern)> Foo<..S, ..P> {
    pub fn new(s: (..S), p: (..P)) -> Self {...}
}

// returns Foo<i32, i32, i32, char, &'a str>
Foo::new((1, 2, 3), ('a', "b"))

EDIT: but it will probably make trait resolution harder, so you're probably right that this should be forbidden.

I updated the RFC concerning the variadic tuple identifiers, and while reading it again, it was very confused. Thanks for the feedback @Aloso.


I did not considered to declare bounds inside the generic parameter bounds. I'll think to add this. This can be possible if the tuples are not declared together

None of those are current syntax. The current syntax is

let [a, b @ .., c] = [1, 2, 3, 4];

And as far as I know this kind of syntax has only ever been usable on slices because of an issue with the layout of subtuples.

Link to the relevant RFC, specifically to a comment I think is particularly relevant to this discussion.


For bundling multiple tuples of the same length I've played with using @@ to invert the top two layers

( ((1,2), (3,4), (5,6)) @@..) == ((1,3,5), (2,4,6))

But it's not something I've fleshed out.

1 Like

Is .. a prefix of the type variable (like variable types in Perl), or a rest/spread operator?

To me it'd make sense as an operator, i.e.:

fn my_func<T>((..v): (..T)) -> T

T is a variable tuple (contains (T1, T2, T3, etc.)), and .. takes T and spreads it into its components.

If .. was used in type position, I'd expect it to capture multiple types:

HashMap<..T>
T == (K, V)

Thanks for the link, this is indeed relevant and useful for this RFC!

I guess I should use the syntax ident @ .. to be consistent.


This depends on the context actually, hence is confusing. I'll use the @ syntax as the link above to describe the difference.

For types, it is a rest/spread operator:

impl<(..(K, V))> MegaMap<(..(K, V))>
//   ^^^^^^^^^^ rest     ^^^^^^^^^^ spread
where ..(K: Hash), {
//   ^^^^^^^^^^^^ spread
    fn get(&self, k: (..K)) -> (..Option<V>) {
//                   ^^^^^ and ^^^^^^^^^^^^^ spread
    }
}

for destructuring, it is more appropriate to have bindings:

{
  let source: (Head, ..Tail) = _;
  // `head` is a variable of type `Head`
  // `tail` is a tuple variable of type `(..&Tail)`
  let (ref head, ref tail @ ..) = source;
}
{
  let mut source: (..L, ..R) = _;
  // `l` is a tuple variable of type `(..&mut L)`
  // `r` is a tuple variable of type `(..&mut R)`
  let (ref mut l @ .., ref mut r @ ..)) = source;
}

Also, this points out that there is an issue in the 2nd pattern: .. is used twice which is not possible. Also, it is not explicit how the compiler should "split" the destructuration of both tuples.

In for loops, it is none. It only indicates you are not iterating with a standard for loop. (Which is currently a pretty bad syntax and need to be changed).

Then I think it should not be an operator, because:

// This function accept all generic types
// and `(..T)` tries to rest the members of T, but we don't know anything about T
fn my_func<T>((..v): (..T)) -> T

// This function only accept tuple types as generic arguments
fn my_func<(..T)>((..v): (..T)) -> (..T)
1 Like

I have updated the RFC with the @ pattern for destructuring. Also added an entry in the "Unresolved questions" section concerning the usage of multiple .. in a single pattern.

Can you elaborate? I did not find precise documentation about this. (except this pre-RFC)

I don't know how it works, but variadic tuples will likely be implemented in the same way as generics, by monomorphozing on each distinct tuple, so handling it in the same way as generics (whatever that is), should be fine.

1 Like

(..T) tries to rest the members of T, but we don't know anything about T

Good point. How about:

fn my_func<T: Variadic>((..v): (..T)) -> T

There would a "magic" trait std::yadayada::Variadic implemented by all tuples, so then we know .. operator can be applied to a tuple (and maybe fixed-size arrays could implement it too, and be spreadable as well?)

1 Like

In that case fn my_func<T: Variadic>((..v): (..T)) -> T and fn my_func<(..T)>((..v): (..T)) -> T are equivalent. I my opinion using (..T) is easier to use.

I feel this is a rabbit hole, If I have to mix tuples and arrays I prefer to have a quite explicit syntax. (So not relying on a trait that both arrays and tuple implements)

To sum up briefly the state of the discussion:

  • The destructure syntax was changed to the syntax used for slice patterns and subslice. (so <ident> @ ..).

  • The for loop syntax to iterate over the members of a variadic tuple is ambiguous with the runtime for loop:

// here, k and maps are variadic tuples
(for (ref k, map) in (k, maps) {
    maps.get(k)
})

See these suggestions as alternatives

I also want to add a new point on the discussion about the .. token in patterns: Its semantic is ambiguous, sometimes it means "catch everything you can" and sometimes it means "catch this explicit variadic tuple". Example:

// Existing destructure pattern
// a = 1, .. = (2, 3), b = 4
// It has the meaning "catch everything you can at this position"
let (a, .., b) = (1, 2, 3, 4);


let variadic_tuple: (Head, ..Tail) = ...;
// Close to existing destructure pattern
// h: Head, and tail: (..Tail)
// The meaning is both:
//   - Catch everything you can at this position
//   - Catch the variadic tuple (..Tail)
let (h, tail @ ..) = variadic_tuple;

// And here comes trouble: 
// If we want to "split" a tuple:
let source: (..L, ..R) = ...;
// A lot of bindings are possible
let (l @ .., r @ ..) = source;

// So maybe we need to explicitly state that we want to match a variadic tuple
// Type ascription may help here:
let (l: (..L) @ .., r: (..R) @ ..) = source;

What's wrong with body == ()?

You are right, I remove the first issue which is not an issue ><

I'm not sure about this. There's a lot of ..s in the code, and they have multiple meanings. Rust already has that kind of asymmetry for & in patterns, where it dereferences instead of referencing, and it turned out to be difficult to learn, and eventually got de-facto removed with match ergonomics.

T: AutoTrait is already an existing pattern, and it's googlable.

I think there are several points here:

Yes, multiple meaning is not good from a UX standpoint. First version of the RFC used .. as prefix to rest and as postfix to expand. Maybe this is something we can redo to solve this ambiguity.

I'd like to have only a single syntax to declare the variadic tuple, and the issue about using a trait bound is that you can't group the declaration of variadic tuples to enforce that they have the same arity. (Like in (..(L, R))). Otherwise, it could be a viable option. For this reason, I still prefer the (..T) syntax.

Considering the for loop syntax, I would be in favor of one of these syntaxes:

// Option 1 - Enclosed in brackets

// Rationale: <> Is like a generic parameter, so a parameter that 
//    is evaluated at compile time

// Iterate over tuple and type
(for (ref k, map) type (Key, Value) in <k, maps> type <K, V> {
    HashMap::<Key, Value>::get(&map, k)
})

(for ref key type Key in <k> type <K> {
    Key::do_something(key)
})

// Iterate over tuple
(for (ref k, map) in <k, maps> {
    map.get(k)
})

(for ref key in <k> {
   key.do_something()
})

// Iterate over type
(for type (Key, Value) in type <K, V> {
    (Key::zero(), Value::one())
})

(for type Key in type <K> {
   Key::one()
})
// Option 2 - Prefixed by @

// Rationale: @ binds to a set of value, 
// the type of the value is determined at compile time

// Iterate over tuple and type
(for (ref k, map) type (Key, Value) in @(k, maps) type @(K, V) {
    HashMap::<Key, Value>::get(&map, k)
})

(for ref key type Key in @k type @K {
    Key::do_something(key)
})

// Iterate over tuple
(for (ref k, map) in @(k, maps) {
    map.get(k)
})

(for ref key in @k {
   key.do_something()
})

// Iterate over type
(for type (Key, Value) in type @(K, V) {
    (Key::zero(), Value::one())
})

(for type Key in type @K {
   Key::one()
})
// Option 2 - Replace `in` by `@` and use `<>` for types

// Rationale: @ binds to a set of value and `<>` designates types variables

// Iterate over tuple and type
(for (ref k, map) <Key, Value> @ (k, maps) <K, V> {
    HashMap::<Key, Value>::get(&map, k)
})

(for ref key <Key> @ k <K> {
    Key::do_something(key)
})

// Iterate over tuple
(for (ref k, map) @ (k, maps) {
    map.get(k)
})

(for ref key @ k {
   key.do_something()
})

// Iterate over type
(for <Key, Value> @ <K, V> {
    (Key::zero(), Value::one())
})

(for <Key> @ <K> {
   Key::one()
})
1 Like

I find it confusing to remove the in keyword, what about:

// Rationale: @ binds to a set of value and `<>` designates types variables

// Iterate over tuple and type
(for (ref k, map) <Key, Value> @in (k, maps) <K, V> {
    HashMap::<Key, Value>::get(&map, k)
})

(for ref key <Key> @in k <K> {
    Key::do_something(key)
})

// Iterate over tuple
(for (ref k, map) @in (k, maps) {
    map.get(k)
})

(for ref key @in k {
   key.do_something()
})

// Iterate over type
(for <Key, Value> @in <K, V> {
    (Key::zero(), Value::one())
})

(for <Key> @in <K> {
   Key::one()
})