Pre-RFC: variadic tuple

Hi, I updated the RFC with a new syntax proposition to replace the expansion of variadic tuples, I think it is easier to understand and make more sense, what do you think?

Also, I dropped the # char in the syntax for clarity and to avoid confusions with attributes.

Now the syntax is ambiguous with range syntax.

Yeah, I very much feel that the "..T" syntax is sort-of ok for types, but definitely not for expressions; plus, I want something more explicit like the above repeat! macro. C++'s expr... is way way too subtle.

Also, can we reverse iteration somehow?

I would imagine that would be implemented as a library? Imagine something equivalent to C++:

template<typename>
struct Rev;
template<typename T, typename... Ts>
struct Rev<std::tuple<T, Ts...>> {
  using type = MergeTuples<
    typename Rev<Ts...>::type, std::tuple<T>>;
 private:
  template<typename T, typename U>
  using MergeTuples = decltype(std::tuple_cat(
    std::declval<T>(), std::declval<U>()));
};
template<>
struct Rev<std::tuple<>> {
  using type = std::tuple<>;
}

static_assert(std::is_same_v<
  std::tuple<char*, void*>, 
  Rev<std::tuple<void*, char*>>::type);

(FWIW, I don't think we'd need to play the stupid games with our equivalent of decltype, since this RFC comes with tuple merging).

1 Like

I'd like to keep the (..T) syntax for types and bounds (or something equivalent), it is quite compact and the range notation is not used at these locations.

But for expression, we may drop the .. syntax for expression, it is quite clear inside the for loop:

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

        let result: (..Option<&V>) = {
            for (ref key, map) type (Key, Value) in (keys, maps) type (K, V) {
                HashMap::<Key, Value>::get(&map, key)
            }
        };

        result
    }
}

However, this does not provide flexibility on the iterators. We may use the iterator syntax which is more flexible but more verbose:

         let result: (..Option<&V>) = {
            for (ref key, map) type (Key, Value) in keys.zip(maps) type K.zip(V) {
                HashMap::<Key, Value>::get(&map, key)
            }
        };

Note: again this is inlined by the compiler and not executed at runtime. Note 2: zip in that case would be a function implemented by the compiler to zip the tuples, we can have the same for rev and other utilities.

In the end, I find the former syntax more explicit but without flexibility and the latter confusing (too close to existing iterator IMO)

In that case, I think that the iteration syntax is fixed and don't provide iterator flexibility, (so option 1). But we may have those features through libraries as @mcy suggested.

Although, this RFC does not provide this flexibility, see below example.

For the reverse case:

trait Rev {
	type Value;
}
impl<T> Rev for T {
	default type Value = ();
    default fn rev(self) -> Self::Value { () }
}
impl<Head, (..Tail)> Rev for (Head, ..Tail) {
	// There is something missing, we need to be able to "destructure" the
	// associated type as a variadic tuple type and use it in the expansion form
	type Value = (#######, Head);

	// Syntax suggestion:
	let type (..RevTail) = <(..Tail) as Rev>::Value;
	type Value = (..RevTail, Head);
	// The let type seems inappropriate as it is acts as an infaillible assign
	// but we know actually nothing about Rev::Value
	// It is not bounded to be a tuple type
	// End

    fn rev(self) -> Self::Value {
         let (h, ..t) = self;
         let (..rev_t) = <(..Tail) as Rev>::rev((..t));
         (..rev_t, h);
    }
}

trait Merge<(..R)> {
	type Value;
}
impl<(..R), T> Merge<(..R)> for T {
	default type = (T, ..R);
}
impl<(..L), (..R)> Merge<(..R)> for (..L) {
	type Value = (..L, ..R);
}

I guess we'd like something like for the implementation

impl<(..(K, V))> MegaMap<(..(K, V))>
where ..(K: Hash), {
	// Destructure the associated type
	// But same issue: Rev::Value is not bounded to be a tuple
    type (..RevK) = <(..K) as Rev>::Value;
    type (..RevV) = <(..V) as Rev>::Value;

    fn get(&self, (..keys): (..K)) -> (..Option<RevV>) {
        let (..ref maps) = &self.maps;
        let (..rev_maps) = <(..&HashMap<K, V>) as Rev>::rev((..maps))
        let (..rev_keys) = <(..K) as Rev>::rev((..keys));

        let result: (..Option<&RevV>) = {
            for (ref key, map) type (Key, Value) in (rev_keys, rev_maps) type (RevK, RevV) {
                HashMap::<Key, Value>::get(&map, key)
            }
        };

        result
    }
}

So maybe investigating destructuring variadic tuple types and / or better expansion for variadic tuple types. Although, I'd like to keep this simple and not introduce a full syntax to create types from variadic tuple. I think if we can "destructure" an associated type as a tuple it will provide quite a lot of features as library.

Traits will act as function: [type] -> type.

Also, I think that some feature would be available only as compiler intrinsics, like:

  • sorted: create a variadic tuple type that has its member order sorted by TypeId
  • unique: create a variadic tuple type where all its member have different types Something like
mod tuple {
	pub trait UniqueTuple {
		type Value;
	}
	pub trait SortedTuple {
		type Value;
	}
	// impl of these traits are compiler intrinsics on all tuple
	pub type Unique<(..T)> = <(..T) as UniqueTuple>::Value;
	pub type Sorted<(..T)> = <(..T) as SortedTuple>::Value;
}

I think you are underestimating how powerful traits are. We could do this to get a library solution.

trait Rev {
    type Value;

    fn rev(self);
}

impl Rev for () {
    type Value = ();

    fn rev(self) -> Self::Value { () }
}

impl<Head, (..Tail)> Rev for (Head, ..Tail)
where (..Tail): Rev,
      <(..Tail)>::Value: Merge<(Head,)> {
    type Value = <<(..Tail)>::Value as Merge<(Head,)>>::Output;
    
    fn rev(self) -> Self::Value {
         let (h, ..t) = self;
         let rev_t = <(..Tail) as Rev>::rev((..t));
         rev_t.merge((h,))
    }
}

trait Merge<(..R)> {
	type Output;
    
    fn merge(self, other: (..R)) -> Self::Output;
}

impl<(..L), (..R)> Merge<(..R)> for (..L) {
    type Output = (..L, ..R);

    fn merge(self, (..other): (..R)) -> Self::Output {
        let (..s) = self;
        (..other, ..s)
    }
}
2 Likes

You are right, we can implement it as a library. To be more consistent with the RFC, it would be like:


// Library for tuple

trait Merge<(..R)> {
    type Value;
}
impl<(..L), (..R)> Merge<(..R)> for (..L) {
    type Value = (..L, ..R);
    fn merge(self, other: (..R)) -> Self::Value {
        (for (l,) in ..(L,) { l }, for (r,) in ..(R,) { r })
    }
}

trait Rev {
    type Value;

    fn rev(self);
}

impl<(..T)> Rev for (..T) {
    default type Value = (..T);
    default fn rev(self) -> Self::Value { () }
}

impl<Head, (..Tail)> Rev for (Head, ..Tail)
where 
    // (..Tail) does implement Rev with the blanket impl
    // But the blanket impl for Merge is only for tuple
    // so we still need require it here
    <(..Tail) as Rev>::Value: Merge<(Head,)> {

    type Value = <<(..Tail) as Rev>::Value as Merge<(Head,)>>::Output;
    
    fn rev(self) -> Self::Value {
         let (h, ..t) = self;
         let rev_t = <(..Tail) as Rev>::rev((..t));
         rev_t.merge((h,))
    }
}

// Usage

impl<(..(K, V))> MegaMap<(..(K, V))>
where ..(K: Hash), {
    // Destructure the associated type
    // However, there is an issue: Rev::Value is not bounded to be a tuple
    type (..RevK) = <(..K) as Rev>::Value;
    type (..RevV) = <(..V) as Rev>::Value;

    fn get(&self, (..keys): (..K)) -> (..Option<RevV>) {
        let (..ref maps) = &self.maps;
        let (..rev_maps) = <(..&HashMap<K, V>) as Rev>::rev((..maps))
        let (..rev_keys) = <(..K) as Rev>::rev((..keys));

        let result: (..Option<&RevV>) = {
            for (ref key, map) type (Key, Value) in (rev_keys, rev_maps) type (RevK, RevV) {
                HashMap::<Key, Value>::get(&map, key)
            }
        };

        result
    }
}

However, in the variadic tuple iteration we need a destructured identifier. So at some point we need to "destructure" the associated type as a variadic tuple. (type (..RevK) = <(..K) as Rev>::Value).

The issue is that here it is faillible as we don't have bound "is tuple".

IMO, to have a safe way to have library like traits to manipulate tuple, we need to enforce that an associated type is a tuple with bounds.

The variadic tuple type destructuration is another story, here it is required because we need it as an identifier for the tuple iteration loop. So we enforce that it is only valid for this kind of item. Otherwise we may find another syntax that can take directly an associated type as argument.

For the tuple bound, maybe we can express it like this:

impl<(..(K, V))> MegaMap<(..(K, V))>
where 
    ..(K: Hash),
    // Bounds the associated type to be a variadic tuple
    // and associate it to an identifier
    <(..K) as Rev>::Value: (..RevK),
    <(..V) as Rev>::Value: (..RevV),
 {
    fn get(&self, (..keys): (..K)) -> (..Option<RevV>) {
        let (..ref maps) = &self.maps;
        let (..rev_maps) = <(..&HashMap<K, V>) as Rev>::rev((..maps))
        let (..rev_keys) = <(..K) as Rev>::rev((..keys));

        let result: (..Option<&RevV>) = {
            for (ref key, map) type (Key, Value) in (rev_keys, rev_maps) type (RevK, RevV) {
                HashMap::<Key, Value>::get(&map, key)
            }
        };

        result
    }
}

This needs more thinking, also about whether we can keep the arity constraint through associated traits? like

impl<(..(L,R))> MyStruct<(..L), (..R)>
where
   <(..L) as Rev>::Value: (..RevL),
   <(..R) as Rev>::Value: (..RevR), {
    // We know that this is valid because
    // L and R have the same arity and the Rev trait keep the arity
    // But the compiler does know only during monomorphization
    fn do_something() -> (..(RevL, RevR)) { ... }
}

I don't think that we need variadic associated types. Also, the solution I posted doesn't rely on specialization, so we won't be blocked on that.

I think we'll need something like Zip to reverse two variadic tuples and maintain parity. We could do something like this, all without specialization or variadic associated types

trait Zip<T> {
    type Output;

    fn zip(self, other: T) -> Self::Output;
}

impl<(..(T, U))> Zip<(..U)> for (..T) {
    type Output = (..(T, U));

    fn zip(self, (..u): (..U)) -> Self::Output {
        let (..t) = self;
        (..(t, u))
    }
}

fn rev_both<(..(T, U, RevT, RevU))>(t: (..T), u: (..U)) -> ((..RevT), (..RevU))
where <(..T) as Zip<(..U)>>::Output: Rev<Output = (..(RevT, RevU))>
{
    let (..(rev_t, rev_u)): (..(RevT, RevU)) = t.zip(u).rev();
    ((..rev_t), (..rev_u))
}

The reason the specialization feature is involved is because of the point made by @pcpthm:

So, we need a witness of the trait impl for recursive implementations.


This can actually work and will be nice.


However, I notice an issue, I need to update the for loop syntax that replace the variadic tuple expansion to allow tuple merging.

Also, a quick note: the variadic tuple are not supported for function, so if we transform your code with a trait, it will be:

trait Zip<T> {
    type Output;

    fn zip(self, other: T) -> Self::Output;
}

impl<(..(T, U))> Zip<(..U)> for (..T) {
    type Output = (..(T, U));

    fn zip(self, (..u): (..U)) -> Self::Output {
        let (..t) = self;
        (for (t1, u1) in ..(t, u) { (t1, u1) })
    }
}

trait RevBoth<(..(T, U, RevT, RevU))> {
    fn rev_both(t: (..T), u: (..U)) -> ((..RevT), (..RevU))
    where 
        <(..T) as Zip<(..U)>>::Output: Rev<Output = (..(RevT, RevU))> {
        let (..(rev_t, rev_u)): (..(RevT, RevU)) = t.zip(u).rev();
        ((for (v,) in ..(rev_t,) { v }), (for (u,) in ..(rev_u,) { u }))
    }
}

If you use trait bounds, you can get around this, see any of my solutions for examples of this.

Why not for functions? You already have to support them for trait functions so this seems like a weird and unnecessary corner case.

This syntax is ambiguous with ranges

1 Like

Actually I don't think so:

[quote="RustyYato, post:48, topic:10878"]

trait Rev { 
    type Value; 
    
    fn rev(self); 
}

impl Rev for () {
    type Value = (); 

    fn rev(self) -> Self::Value { () } 
} 

impl<Head, (..Tail)> Rev for (Head, ..Tail) 
where (..Tail): Rev, 
      <(..Tail)>::Value: Merge<(Head,)> {
    type Value = <<(..Tail)>::Value as Merge<(Head,)>>::Output;

    fn rev(self) -> Self::Value { 
        let (h, ..t) = self; 
        let rev_t = <(..Tail) as Rev>::rev((..t)); rev_t.merge((h,)) 
    }
}

In that example, the implementation impl<Head, (..Tail)> Rev for (Head, ..Tail) requires (..Tail) being Rev but there are no witness of this impl.

See @pcpthm's post for the original explanation.


The issue concerning the function is that it allows recursive implementations. But in current rust, you can't override or specialize a function.

fn arity<Head, (..T)>() -> usize {
    arity::<(..T)>() + 1
}
// How do you define the termination?
// We need to define a specialized implementation for `()` here
// But trying to include it in this RFC will dramatically increase its scope and complexity.
// So, maybe in a future RFC

Yes, I still keep the (id,) notations as ranges can't have bounds defined by a tuple, but still pretty close. For now I don't have a better idea without having something quite ugly nor inconsistent.

But If you have a better suggestion feel free to share it :wink: !

Saying where (..Tail): Rev is the witness, this is exactly how crates like typenum and frunk work. You didn't put any where bounds in any of your original examples, that may not have worked.

This will work

impl Arity for () {
    default const VALUE: usize = 0;
}
impl<Head, (..Tail)> Arity for (Head, ..Tail)
    // this where bound is important
    where (..Tail): Arity {
    const VALUE: usize = <(..Tail) as Arity>::VALUE + 1;
}

Because by saying (..Tail): Arity you are asserting that (..Tail) must implement Arity. The compiler can check this before monomorphization, and give you nice error messages.

Not all functions like this will require recursion, so this isn't a good reason to deny them. If you need recursion, then you can extract that out to a trait, but if you don't then you can just use a free function. For example, by rev_both function does not require recursion on it's own (Rev needs it, but that is nicely extracted out)

Also using the loop syntax, we won't need recursion for many applications of variadic tuples.

..(rev_t,) is a valid range.

I don't have any better syntax, but I just wanted to note that it is ambiguous so that other readers can start to think of an alternate syntax.

1 Like

Thanks, I misunderstood the issue, I will update the RFC accordingly.

I'll update the RFC to mark this as an issue to resolve.

I agree with this, recursion is a specific case. However, if the function is recursive, the compiler must issue an error as this case is not handled.

1 Like

Yes, this is a good idea!

Hi updated the RFC with the last feedbacks:

  • support for functions without recursion
  • trait library utilities and their usage

Considering the ambiguous range syntax, I have several suggestions:

Option 1 - Original

(for (ref k, map) type (K, V) in ..(k, maps) type ..(K, V) {
    HashMap::<K, V>::get(&map, k)
})

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

This one is ambiguous with ranges

Option 2 - loose the ..

(for (ref k, map) type (K, V) in (k, maps) type (K, V) {
    HashMap::<K, V>::get(&map, k)
})

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

In my opinion, this is dangerously close to standard for loop. Although the compiler can makes the different because variadic tuple identifier are used, but for a human, this is ambiguous.

Option 3 - Enclosed in brackets

(for (ref k, map) type (K, V) in <k, maps> type <K, V> {
    HashMap::<K, V>::get(&map, k)
})

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

Option 4 - No delimiters

(for (ref k, map) type (K, V) in k, maps type K, V {
    HashMap::<K, V>::get(&map, k)
})

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

Option 5 - Enclosed by dot

(for (ref k, map) type (K, V) in .(k, maps). type .(K, V). {
    HashMap::<K, V>::get(&map, k)
})

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

Option 6 - Enclosed by flipping table

(for (ref k, map) type (K, V) in (╯°Д°)╯(k, maps)/(.□ . \) type (╯°Д°)╯(K, V)/(.□ . \) {
    HashMap::<K, V>::get(&map, k)
})

(for ref key type Key in (╯°Д°)╯k/(.□ . \) type (╯°Д°)╯K/(.□ . \) {
    Key::do_something(key)
})

Option 7 - Prefixed by @

(for (ref k, map) type (K, V) in @(k, maps) type @(K, V) {
    HashMap::<K, V>::get(&map, k)
})

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

Option 8 - Prefixed by a dot

(for (ref k, map) type (K, V) in .(k, maps) type .(K, V) {
    HashMap::<K, V>::get(&map, k)
})

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

Option 9 - Prefixed by &~

(for (ref k, map) type (K, V) in &~(k, maps) type &~(K, V) {
    HashMap::<K, V>::get(&map, k)
})

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

What do you think of theses?

3 Likes

What about for type $pat in $expr { .. } with for type <$ty> in <$ty> as the type-only version?

I though to use the $ symbol, but I am afraid it will be confusing when using macros. Like when you want to implement a macro_rules that uses variadic tuples

macro_rules! impl_tuples {
    ($($ident:ident),*) => {
        $(
        impl<(..T)> $ident for (..T) {
           fn my_function((..t): (..T)) {
               // It will be ambiguous at this point
               (for $tval in $t { $tval })
           }
        }
       )*
    }
}

Hence I am not keen to use $.

On the note of dylibs,I think variadic tuples should be handled in the same way as generics.