Pre-RFC: variadic tuple

Yes, but how often do single element tuples come up? I don't think that I have ever used them. That msy change with this proposal, but I don't expect it to use them often.

Actually, I use them quite a lot because I use extensively tuple types for zero cost based abstractions. It was weird the first time I ran into it, but I get used to

1 Like

If the intention is for this feature to be used for variadic generics, then we absolutely are going to be needing to write (A,) all the time, so I think this is an important consideration for this proposal.

1 Like

Wouldn't a pattern bind make the most sense (within the confines of existing rust syntax), e.g. input: (i@..#T)? On the other hand that's a lot of symbol soup...

With generalized ascription in argument position, allowing a pattern bind like that ((i @ ..#T): (..#T)) would then also allow the much more reasonable (i: ..#T). Generally, I don't think any patterns allow binding outside and inside (I may be wrong with ref mode) of some object, so you'd refer to the full thing as (i,)..#T (or whatever syntax).

@bbatha I don't feel comfortable with a pattern bind because the binding is basically a restriction of the matches a pattern can perform, and this is not what we desire here.

This is closer to a destructuration or type ascription for arguments (as @CAD97 pointed out).

So I'd still go for both syntax, either index: (..#T) or (..#v): (..#T), it is more consistent with existing syntax.

I refactored the RFC to make it easier to understand and also included from the previous feedbacks:

  • Variadic tuple types and variadic tuples (aka C++ type parameter packs and function parameter packs)
  • Destructuration of variadic tuples

TODO: Add an error when multiple independent variadic tuple identifier are used in a single expansion form. ie: fn my_func<(..#K), (..#V)>() -> (..#HashMap<K, V>) { ... } -> K and V may have different arities.

FWIW, C++ allows this, provided the arities are equal, in order to be able to create zip-like functions.

To handle this two options were considered:

  • Have a single syntax to declare variadic tuples and allow multi-identifiers expansion forms and issue an error at the monomorphize phase when the arities are different.
  • Have two syntaxes with one enforcing that tuples have same arities (ie: (..#(T1, T2))) and only check that multi-identifier expansion form uses variadic tuple declared together. (And this can be done very early).

I choose the latter because it is more explicit when writing the code. So you can still write zip like functions, but the syntax ensure that tuple arities matches.

3 Likes

@RustyYato @CAD97 @mcy @bbatha @kornel @lordan What do you think of the last version?

Also I am thinking of separating the function case: there is currently no way to define a specialization of a fn (which is needed for recursion termination here). If needed variadic tuple in function generic parameters can be defined in another RFC.

What do you think?

Well, this proposal is quite close to C++ parameter packs, and as such I'd not be unhappy if it (or something like it) became part of the language.

That said, while straightforward, I can't shake the feeling that Rust could actually do better, using higher-kinded types and type-level functions.

7 Likes

I have a few observations:

  • You discuss recursing on the arity of a tuple by writing
fn foo<H, (..#T)>(_: (H, ..#T)) {
  foo((..#T));
}
fn foo<()>(_: ());

Unfortunately (or, perhaps, fortunately), Rust doesn't have overloading or ADL or function specialization, so you're going to need a better story for this part of your proposal.

  • In C++, parameter packs are kind of painful in a lot of ways that the STL has to work to alleviate. How do you propose writing a function (in the presence of const generics) like C++'s std::get? E.g.,
fn nth<const N: usize, (..#T)>() -> ??
  where (..#T): ArityAtLeast<{N}>;

(My main point here is that a feature like this is mostly useless without significant library support for doing trivial manipulations; maybe this should just be filed under "Further Work", but it's food for thought.)

  • Have you contemplated heterogenous iteration? E.g., something like
fn foo<(..#Ts)>(ts: (..#Ts))
where ..#(Ts: Trait) {
  for T in <(..#Ts)> {
    // Here T is an APIT, syntax sugar (..#{ .. }).
  }
  for t in ts {
    // Similarly, t: impl Trait here.
  }
}

It's not clear to me that these constructs should look exactly like your usual for loops, but I really want to avoid the unreadable mess that are parameter pack expansions in C++.

  • Finally, I think it's worth starting a bikeshed over the syntax, now that the general shape of the proposal has crystalized. (..#T) is ok for discussing the RFC but terrible surface syntax. I personally would prefer the C++ syntax, but unfortunately expr.. is taken and expr... is uncomfortably similar. If we manage to ditch (..#expr) altogether and use heterogenous iteration instead, we would get to use fn foo<T..>()... though the question of patterns remains in the air.
2 Likes

I am not a fan of the the ..#T syntax, for several reasons:

  1. It would most definitely break the procedural macro of the quote crate
  2. It's confusing when T is an array
  3. It looks noisy
  4. It diverges from current syntax
  5. With variadic tuple expansion, it's inconsistent and obscures control flow

1. It would most definitely break the macro of the quote crate

The quote crate has a quote! macro that substitutes tokens starting with a hashtag. It's used frequently for creating procedural macros.

2. ..#[T; n] looks like an attribute

Although it might not cause a parse error, it's definitely weird to look at.

3. It looks noisy

Example from the RFC:

fn clone_add<(..#T)>((..#i): (..#T)) -> (..#T) 
where ..#(T: Clone + Add) {
  (..#(<T as Clone>::clone(&i) + i))
}

4. It diverges from current syntax

The syntax of destructuring a variadic tuple is different from destructuring a normal tuple or a struct:

let Foo { a, ..b } = normal_struct;
let Bar(a, ..b) = tuple_struct;
let (a, ..b)  = tuple;
let (a, ..#b) = variadic_tuple;

I believe that this will make the feature harder to learn, understand, and use correctly.

5. With variadic tuple expansion, it's inconsistent and obscures control flow

Example from the RFC:

fn my_func<(..#T)>((..#i): (..#T)) {
  (..#{ println!("{}", i) })
}

What is i here? It's not a tuple, it's what is inside the tuple. But it isn't a value either. It's a special case, that doesn't exist anywhere else, and it can only be used in an expansion form (..#EXPRESSION). I can only imagine how much effort this would be to implement in the compiler.

And what about the expansion form? It "desugars" into a tuple, and the content of the expansion form is repeated n times, where n is the arity of the tuple:

// if T has an arity of 3:
fn my_func<T1, T2, T3>((i1, i2, i3): (T1, T2, T3)) {
  (
    { println!("{}", i1) },
    { println!("{}", i2) },
    { println!("{}", i3) },
  )
}

This means that three values are printed, even though the function contains println! only once, and doesn't have any loops. Also, the expansion form can't take ownership of a value in the function, because then there would be more than one owner. People will find this counter-intuitive.

It would be more intuitive to use a block that introduces the variable, so it has a well-defined scope. Something like:

fn my_func<(..#T)>(tuple: (..#T)) {
  (repeat i in tuple {
    println!("{}", i)
  })
}

However, a macro might be more appropriate for this.

P.S. You're missing a T: Display trait bound in the examples!

4 Likes

@mcy @Aloso, thanks! I appreciate the feedbacks.

Considering the issues pointed out:

  1. function recursion

I think its is a bad idea to include support of variadic tuple in generic parameter group of functions. Mainly because there is no way to specialize an implementation for a function currently in Rust.

  1. Utilities

I totaly agrees, utilities to manipulate tuple will be very valuable. Although those are not required for this RFC to have value, so I prefer to mention these in the Future section for future RFCs.

  1. Syntax

Yes that is ugly, but I needed something to discuss with to establish the main features of the RFCs. So now is the time to think about a nice readable syntax that is way easier to understand and maintain.

It would be nice to have something similiar to existing syntax and way of thinking, but still noticeable that its does not run any code at runtime.

Considering expansion, the current thinking model is macro like with expanded patterns, but maybe we can instead use imperative and functional syntaxes which are easier to understand. (To replicate for loops or iterators).

Still, I need some time to think about it and come up with several ideas.

Recursion

To implement some feature, we may want to use recursion over the arity of the tuple. For instance, let's implement a trait that gives the arity of a tuple as a const value:

trait Arity {
    const VALUE: usize;
}

impl<Head, (..#Tail)> Arity for (Head, ..#Tail) {
    default const VALUE: usize = <(..#Tail) as Arity>::VALUE + 1;
}
impl Arity for () {
    const VALUE: usize = 0;
}

I guess this example won't be work for Rust because Rust want to check generics before monomorphization.

While checking validity of <(..#Tail) as Arity>::VALUE + 1, there is no witness of (..#Tail) being Arity. Instead we could use specialization to ensure all variadic tuple type have instance of Arity:

impl<(..#Ts)> Arity for (..#Ts) {
    default const VALUE: usize = 0;
}
impl<Head, (..#Tail)> Arity for (Head, ..#Tail) {
    const VALUE: usize = <(..#Tail) as Arity>::VALUE + 1;
}

Similarly, the variadic tuple expansion can't be just an "expression template" and there must be a static semantics to be given. For example,

fn test<(..#Ts)>((..#t): (..#Ts)) {
  let s = String::new();
  (..#{ drop(s); t });
}

Defining test should be error because test<(i32, i32)>((0, 0)) expands to a double drop. However, expanded test<(i32)>((0,)) is valid and thus it is an approximate semantics in the sense.

Version 1 - Original

struct MegaMap<(..#(K, V))> {
  maps: (..#HashMap<K, V>),
  keys: (..#[K; 32]),
  values: (..#[V; 32]),
}

impl<(..#(K, V))> MegaMap<(..#(K, V))> {
  fn get(&self, (..#k): (..#K)) -> (..#Option<&V>) {
    let (..#(ref maps)) = &self.maps;
    (..#maps.get(k))
  }
}

impl<(..#T), Last> Hash for (..#T, Last) 
where
    ..#(T: Hash),
    Last: Hash + ?Sized, {

    #[allow(non_snake_case)]
    fn hash<S: Hasher>(&self, state: &mut S) {
        let (..#(ref v), ref last) = *self;			 
      	
        (..#v.hash(state), last.hash(state));   
    }
}

Syntaxes improvements options

Loosing the '#' char

The range syntax .. defines instance not types, so when a type is expected .. can't be used as a Range.

So, we can use the syntax:

struct MegaMap<(..(K, V))> { }
}

impl<(..(K, V))> MegaMap<(..(K, V))> {
  fn get(&self, keys: (..K)) -> (..Option<&V>) { }
}

impl<(..T), Last> Hash for (..T, Last) 
where
    ..(T: Hash),
    Last: Hash + ?Sized, {

    #[allow(non_snake_case)]
    fn hash<S: Hasher>(&self, state: &mut S) { }
}

Destructuring

We can match the existing syntax with ..ident:

Drop the #, directly use ..ident or ..ref ident or ..ref mut ident

So we can write instead:

let (head, ..tail): (Head, ..Tail) = _;
let (ref head, ..ref tail): (&Head, ..&Tail) = _;
let (ref mut head, ..ref mut tail): (&mut Head, ..&mut Tail) = _;

Variadic tuple expansions

Another of thinking the expansion is to use similar syntax for iterators and loops. Something like:

impl<(..(K, V))> MegaMap<(..(K, V))>
where ..(K: Hash), {
  fn get(&self, (..k): (..K)) -> (..Option<V>) {
    let (..ref maps) = &self.maps;

    // iterator like syntax
    k
      // iterate over the members of tuple `(..k): (..K)`
      .into_iter()
      // adaptor to zip with tuple `(..maps): (..&HashMap<K, V>)`
      .zip(maps.into_iter())
      // map a generic fn: `fn<K: Hash, V>(k: K, map: &HashMap<K, V>) -> Option<&V>` 
      .map(|k, map| map.get(k))
      // collect into a tuple
      .collect::<(..Option<&V>)>()
  }

  // Note, the map operation reminds higher kinded type:
  // `fn map<M: for<T, V> Fn(T) -> V>(mapper: M)`
}

impl<(..T), Last> Hash for (..T, Last) 
where
    ..(T: Hash),
    Last: Hash + ?Sized, {

    #[allow(non_snake_case)]
    fn hash<S: Hasher>(&self, state: &mut S) {
        let (..ref tuple, ref last) = *self; 
        // `tuple` is a variable with type `(..&T)`
        
        // imperative style syntax
        for member in tuple {
          member.hash(state);
        }
        // equivalent to
        // `tuple.into_iter().for_each(|member| member.hash(state));`
        last.hash(state);
    }
}

To continue on this way, I think it is best to focus on the iterator syntax, the imperative style syntax can be deduced from it as syntaxic sugar.

For now, I'll investigate the state of higher-kind types and if this can help this RFC as well.

But so far, what do you think of these options?

3 Likes

Thanks for the feedback, indeed that is more consistent with current behaviour, I'll update the RFC accordingly.

I am going to rework this part, taking this into account and trying to find a more consistent and easier to use syntax.

Because tuples are heterogeneous, we could only iterate over an Iterator<Item = &dyn T> or Iterator<Item = Box<dyn T>.

I think the best way forward (without breaking backwards compatibility) is with a built-in macro:

repeat!(member in tuple {
    member.hash(state);
});

This macro could even build a new variadic tuple with the same arity.

1 Like

To be clear, I am not suggesting we make tuples iterable in the traditional sense; I'm suggesting that we add a new construct that does something along the lines of your macro-like construction (but, frankly, should maybe be more first-class than some macro thing)...

Making tuple iterable is not the intent, it was more to start thinking of the variadic tuple as an heterogeneous array that could by iterated on with a specific syntax.

But I agree that it should be first class citizen otherwise it will be a real pain to use. (that is exactly the case for the first version of the RFC).

But still needs to find an appropriate syntax that fits the Rust language and won't be confused with traditional iterators