I'm the author of the previous attempt at this. Here are my thoughts:
i think there’s no reason not to limit the scope of variadic generics to tuples:
[...]
Variadic<(A, B, C)> // is valid syntax
Variadic<((A, B), C)> // is also valid
Variadic<A, B, C> // too many params 😡
There's one big reason: backward-compatibly making iter::Zip
and co. variadic.
if you allow traits to be generic parameters
Sounds like a huge change, and if the Tuple
trait needs to be magic anyway, I don't think it is justified.
however, there’s still one blind spot in this design:
fn variadic<T: (..), U: (..)>(params: (..T, ..U)) -> (T, U);
let (left, right) = variadic((0, 1, 2));
// what does this print?
println!("{left:?} vs. {right:?}");
i don’t think it would be too difficult to enforce that when inference converts the type (i32, i32, i32)
into (..T, ..U)
it greedily generates T
, so U
is reduced to ()
. this seems like the most sane approach to me, though we could also try to make this a compiler error (i don't think its possible without violating semver somehow).
I don't see the semver issue?
trait solving
It's good that you put effort into actually specifying an algorithm, I didn't bother with that at all. So kudos to you!
trait Heapify {
type Boxed: (..);
}
impl Heapify for () {
type Boxed = ();
}
// there's no overlap with the above impl
// because this one has a length range of `1..`
// and the other has an exact length of `0`.
impl<T, U: (..)> Heapify for (T, ..U) {
type Boxed = (Box<T>, ..(<U as Heapify>::Boxed));
}
Smart, I like it. I do think people will want an explicit iteration syntax eventually, using recursion for everything would get annoying. But MVPs are allowed to be annoying, as long as they work and don't get held up in endless bikeshed.
trait MyNumsSummed {
type Output: (..);
}
impl MyNumsSummed for () {
type Output = ();
}
impl<T: Add, U: (..Add)> for (T, ..U) {
type Output = (<T as Add>::Output, ..(<U as MyNumsSummed>::Output))
// `forall<U> { U: MyNumsSummed }` is provable given `U: (..Add)`,
// using the rules defined in "trait solving#candidate selection".
}
fn sum_my_nums<T: (..Add)>(xyz: T, abc: T) -> <T as MyNumsSummed>::Output {
match (xyz, abc) {
// both `xyz` and `abc` are empty; we have no work left to do.
((), ()) => (),
// both `xyz` and `abc` have > 1 element.
// we add what we can and recurse using the remaining elements.
(
(x, y_etc @ ..),
(a, b_etc @ ..),
) => {
let x: impl Add = x;
let y_etc: impl (..Add) = y_etc;
compose!(x + a, ..sum_my_nums(y_etc, b_etc))
},
// the tuples aren't the same length.
// perhaps the compiler will be able to detect that `xyz` and `abc`
// resue `T` and so are gauranteed to be the same length
// but this isn't necessary for an MVP.
_ => panic!("you've been a naughty boy!"),
}
}
My proposal has a note about wanting to have some sort of match
syntax, but I hadn't though of using associated types on the type-level side; that's smart. One question is, how would this work with type inference? If you don't explicitly annotate the output type of the match
, is the compiler going to automatically generate the needed trait + impls? What about IDE type hints?
Also, as with my previous comment, people will want an explicit iteration syntax eventually, even if it's not in the MVP.
lifetime packs
the most recent popular variadic generics proposal included the concept of "lifetime packs" (i.e. variadic lifetimes) and similar ideas for const generics. i'm going to be honest when i say that i just don't really understand what the point is or how it works, but even despite that, i think the functionality can be mirrored with what i call the "smuggler pattern":
I don't love lifetime packs either; they were the least sucky way I could find to fill a hole. Basically, there are already tuples of values, and tuples of types, but no way of representing a list of lifetimes. So I added one, because I needed it to fit lifetimes into the rest of the proposal. Maybe it can be emulated through trait wizardy? That's never fun, but if lifetime variadics are sufficiently rare, it could be Good Enough?
i think whether or not [a variadic number of parameters for a function] should be valid creeps far out of the scope variadic generics as specified in this document. personally, i think its pretty unRusty as it easily allows overloading
You can also hack overloading into Rust with custom impls of the (unstable, sure) Fn
traits. I don't think it will be a problem, as long as it's not deliberately made easy, and there is a community standard of not doing it.