Pre-RFC: variadic tuple

The $ is not literal, it's a common pattern around here to represent "this production here" with the macro capture syntax.

for (ref k, map) type (K, V) in ..(k, maps) type ..(K, V)

I feel like this is much more verbose than necessary. The compiler knows the type of k and maps, so it can infer the type of (k, maps) and (ref k, map).

Maybe this would work:

for (ref k, map) repeat in ..(k, maps) { ... }

So repeat would be context-sensitive keyword. @mcy suggested for type instead, but that's not very descriptive for this construct.

Why don't you want to use a macro? A macro gives us all the flexibility we want:

repeat!((ref k, map) in ..(k, maps) { ... })

A few more notes

There are still a few issues, mistakes and inconsistencies in the RFC, besides the ambiguous syntax:

  • Declaration:

    The outer parentheses in <(..(T1, T2, ..., Tn))> seem to be required, although they're unnecessary in a where clause.

    I propose to always omit the outer parentheses in generics, e.g.

    struct MegaMap<..(K, V)>
    

    However, in variadic tuple types, they're still required:

    fn foo<Head, ..T>(bar: (..T), baz: (Head, ..T)) {}
    
  • "Expansion" section:

    fn append<(..L), (..R)>(l: (..L), r: (..R)) -> (..L, ..R)
    // should be
    fn append<(..(L, R))>(l: (..L), r: (..R)) -> (..L, ..R)
    
  • "Destructuring" section:

    fn my_func<(..T)>((..v): (..T)) -> (..T)
    // I guess is equivalent to
    fn my_func<(..T)>(v: (..T)) -> (..T)
    
  • "Iterating over variadic tuple" section:

    • Same thing in the get function
    • hash function:
      for member in ..(tuple,) { ... }
      
      Why do you use a tuple: (Hash,) on the right and a member: Hash on the left? Shouldn't they be the same, e.g.
      for member in ..tuple { ... }
      // or
      for (member,) in ..(tuple,) { ... }
      
    • merge function:
      let (..l) = self;
      
      Is this really required? I guess I could use self directly instead of l.
  • Reference-level explanation

    • I'm missing a more formal definition of the syntax here!

Should variadic tuples support impl Trait? E.g.

fn join(tuple: (..(impl ToString))) -> String;
fn foo() -> (..(impl Iterator<Item = u64>));

No, this is correct as is. This is tuple concatenation, so the lengths of the two tuples may differ.

For the initial impl, no. This can be expanded upon later, as it isn't obvious what the semantics should be.

Yes exactly, the syntax suggested is the full syntax.

for (ref k, map) repeat in ..(k, maps) { ... }

Is perfectly valid.

The intent is to showcase that you can as well iterate on variadic tuple type. This example will have more sense:

trait Integer {
	fn one() -> Self;
}

fn add_one<(..T)>((..t): (..T)) -> (..T)
where
	..(T: Integer + Add), {
	(for t type T in (..t) type (..T) { t + T::one() })
}

You need a type variable to perform this.

I will add this example in the RFC


IMO tuple is a strength of Rust and should be first class citizen, so fully integrated in the language.


I disagree, because it will prevent to have multiple variadic tuple with different arities.

fn append<..L, ..R>() -> (..L, ..R) { ... }

The compiler can't deduce when L ends and R starts.


They are close but not equivalent:

// Here `(..v)` destructure the variadic tuple with an identifier `v`
// So we can use `v` in for loop to iterate over its members
fn my_func<(..T)>((..v): (..T)) -> (..T)
// Here `v` is a variable identifier with a type `(..T)`, it can't be used in a for loop to iterate over its members
fn my_func<(..T)>(v: (..T)) -> (..T)

You are correct, I will fix the issue.


Yes this is required, because of the above comment: self can't be used in a variadic for loop but l can. This is the same behaviour when destructuring structs or tuple with a self function call.

Example:

trait SpecialInto<T> {
	fn special_into(self) -> T;
}

impl SpecialInto<usize> for (usize, usize) {
	fn special_into(self) -> usize {
		let (a, b) = self;
		a + b
	}
}

This way you can take ownership of the members of a struct / tuple.


Working on it!

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 == ()?