Variadic generics - pre-RFC

Why would tuplezip compile. Wouldn’t it be ambiguous to call?

2 Likes

Also, can i specify that two tuples are supposed to be the same length?

1 Like

Thanks, fixed.

No you can't, but you could instead accept a tuple of pairs.

If I am not wrong, it is not possible to get the length of the variadic tuple, right? I was thinking something like C++ sizeof...(A).

If getting the length could be possible, then I could use something like static_assertions to explicitly generate compile errors if the length of the tuples are different from what I expect.

1 Like

Seems that could be handled by the recursion needed to use the tuples together, e.g. something like

fn log<Label, Value, Labels, Values>(
    (label, ...labels): (Label, ...Labels),
    (value, ...values): (Value, ...Values),
)
where
    Label: Display,
    Value: Debug,
    Labels: (X: Display; X),
    Values: (X: Debug; X)
{
    eprintln!("{}: {:?}", label, value);
    log(labels, values);
}

Although, that would be a monomorphisation time error. It seems like there needs to be some way to bound tuple lengths to truly support something like this. Also, I’m not sure how this could handle the base case; what happens once labels == () && values == (), it would attempt to call log((), ()) which would error because it requires at least 1 element in each tuple?

@memoryleak47 is the intention to support recursively defined functions like this, or am I just trying to do stuff that is out of scope?

1 Like

It could be worth noting that, even if it is nice if we support compile-time recursion like the one you showed, it could be important to support variadic fold expressions. Using the example from @Nemo157 as a base, something like

fn log<...Labels, ...Values>(labels: (...Labels), values: (...Values))
where
    Labels: (X: Display; X),
    Values: (X: Debug; X),
{
    ...eprintln!("{}: {:?}", labels, values);
}

This, intuitively, should resolve in something like

fn log<A, B, C, D, E, F>(labels: (A, B, C), values: (D, E, F))
where
    A: Display, B: Display, C: Display,
    D: Display, E: Display, F: Display,
{
    eprintln!("{}: {:?}", labels.0, values.0);
    eprintln!("{}: {:?}", labels.1, values.1);
    eprintln!("{}: {:?}", labels.2, values.2);
}

This if we consider supporting multiple tuple unfolding in a single unpack.

There are two main reasons I am thinking about this possibility, based on my experience in C++.

The first thing because we are missing many things to access specific tuple elements using nonliteral notations. In details, at to date we must write t.N, where N is a valid index for the tuple. Unfortunately this doesn’t compose so well in terms of generic programming. Moreover, we still don’t have non-type generics, precluding the possibility of using compile-time indices to access specific elements of a tuple. Finally, variadic templates existed since C++11, but until the introduction of std::integer_sequence in C++14 writing folding/recursive things like in the example was a real pain.

In Rust we have a powerful macro ecosystem, and maybe it is possible to assess some of these situations without compiler support. Nevertheless, I would really like a deep analysis of what this proposal could allow by itself, which situations could be supported from external crates (with macros and procedural macros) and what will not be possible without further improvements.

Ok, second observation. This is a minor issue from some points of view, a major one from others. C++17 introduced fold expressions for two main reasons: better code and better compile-time performance. Obviously I am thinking about the compilation times. Indeed, before fold expressions all the template instantiations were performed recursively, and this caused a huge impact on compile performances.

If we introduce variadic generic with the support for recursiveness but without folding expression support, I think that we will end up with slowdown at compile-time very soon (everything that nowadays is implemented using macro builders will probably use variadics).

Sorry for the long post, but I am quite interested in this proposal, and I would like to fully understand the actual possibilities and aims.

EDIT: ok, maybe it is good to show some example of what can be done in C++, with the good, the bad and the ugly syntax :wink:

// Return true if all values can be implicitly converted to bool(true)
template <typename... Ts>
auto all_of(Ts... ts) {
    return ( ts && ... );
}

// Sums all the values, starting from 0
template <typename... Ts>
auto sum_all(Ts... ts) {
    return ( 0 + ... + ts );
}

// Sums all the values, but the first value is the first given parameter
template <typename T, typename... Ts>
auto sum_all2(T t, Ts... ts) {
    return ( t + ... + ts );
}

// Print all the elements without separators
template <typename... Ts>
void print_all(Ts const &... ts) {
    (std::cout << ... << ts);
    std::cout << '\n';
}

// Print all the elements using a space as separator
template <typename T, typename... Ts>
void print_all_sep(T const & t, Ts const &... ts) {
    std::cout << t;
    ((std::cout << ' ' << ts), ...);
    std::cout << '\n';
}

// Print the index for every element and the element itself for every line
template <typename... Ts>
void print_all_with_index(Ts const &... ts) {
    // This not even C++17, it's C++20...
    []<std::size_t... Indices>(auto&& args, std::index_sequence<Indices...>) {
        ((std::cout << Indices << ' ' << std::get<Indices>(args) << '\n'), ...);
    }(std::make_tuple(std::cref(ts)...), std::make_index_sequence<sizeof...(Ts)>());
}

As you can see, the first 4 example are quite nice, but the others… :sob:

2 Likes

A further question (I just realized): @memoryleak47 what about variadic generic lambdas? Do you want to avoid them for now or you have some plan to support them?

This would also suggest an alternate possibility for the bound syntax:

fn log<...Labels, ...Values>(labels: (...Labels), values: (...Values))
where
    ...(Labels: Display),
    ...(Values: Display)

The problem, though, is that both this and your proposal rely on making parameter packs unique and magic, like in C++, as opposed to ... being just a way to convert back and forth between parameter lists and (otherwise ordinary) tuple types/values.

3 Likes

I think generic closures have implementation problems related to monomorphization, so I would kick this down the road.

1 Like

I don’t see why generic closures would have problems with monomorphization given that generic methods work fine.

I think it is the lack of typed hrtb that cause closures not to be generic right now.

Example: for<T:Debug> |v:&T|{ println!("{:?}",v) }

Example constraint: F:for<T:Debug> Fn(&T)

Here is some previous discussion on generic closures, I think they also decided that it wasn’t possible due to a lack of HRTB. Also, while generic clousures would really benefit this RFC, I don’t think it is necessary to put it in with this RFC. It would be better to split it off into it’s own RFC.

1 Like

I'm a bit concerned by syntax (T; T) for tuple bounds which as far as I understand introduces formal generic parameters without angle brackets. I think it is unprecedented in Rust. Have you considered any variants like (<T> ..T)? I would also expect a where clause as an optional part of this syntax in order to express complex bounds.

2 Likes

It is definitely intended to support recursively defined variadic functions. Yet I’m not certain whether this pre-RFC requires additional features to accomplish that goal.

There was already something similar to a where-bound, but I recognize that literally using where is the nicer solution. I changed the pre-RFC accordingly, it now allows the use of where-clauses. How would you represent ((A,B); A: Clone, B: Copy) with the (<T> ..T)-notation?`

<A: Clone, B: Clone> ..(A, B)


This looks a lot like higher rank/kinded type bounds.

That would be either:

  • (<A: Clone, B: Copy> ..(A, B)) (apparently @RustyYato forgot the enclosing parens)
  • (<A, B> ..(A, B) where A: Clone, B: Copy) with a where clause.

Now rethinking about it, there is something unsatisfying about my proposal (though unrelated to angle brackets): in cases where no formal parameter is needed we would have for example (..u32). This is certainly inconsistent with the semantic proposed here where a leading .. means "remove the parens from the following tuple type". Character ; as in your proposal works indeed but I don't like it so much (this is certainly a much more minor concern). I suppose the idea is to mimic array syntax [u32; 4]. I feel like dot sequences convey more naturally the idea of producing a sequence of items matching a prototype. A solution may be to use a trailing ...

To clarify with some examples:

  • By keeping the proposed trailing ;
    • (u32;),
    • (<T> T;),
    • (<A: Clone, B: Copy> (A, B); )
    • (<A, B> (A, B); where A: Clone, B: Copy)
  • By using a trailing ..:
    • (u32..)
    • (<T> T..),
    • (<A: Clone, B: Copy> (A, B)..)
    • (<A, B> (A, B).. where A: Clone, B: Copy)
2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.