This is an attempt at a summary of existing proposals for Rust variadic templates.
Variadic templates (aka variadic generics, or variadic tuples), are an often-requested feature that would enable traits, functions and data structures to be generic over a multitude of types, instead of a single type.
To give a quick example, a Rust variadic function might look like this:
fn make_tuple_sing<...T: Sing>(t: (...T)) {
for member in ...t {
member.sing();
}
}
let kpop_band = (KPopStar::new(), KPopStar::new());
let rock_band = (RockStar::new(), RockStar::new(), RockStar::new(), RockStar::new());
let mixed_band = (KPopStar::new(), RockStar::new());
make_tuple_sing(kpop_band);
make_tuple_sing(rock_band);
make_tuple_sing(mixed_band);Note that variadic generics are a broader feature than variadic functions. There are many languages implementing a feature that lets user call a function with an arbitrary number of parameters, which are sometimes called variadic functions. The extra parameters are dynamically typed (C, JS, Python) or a shorthand for passing a slice (ex: Java, Go, C#).
As far as I'm aware, there are only two widespread languages implementing variadic generics (C++ and D), which is what this document is about.
This has been the focus of much attention, as the use case is a natural extension of the one that motivated const-generics.
Currently, implementing a trait for a tuple is usually done by writing a macro implementing the trait over a given number of fields, then calling the macro multiple times.
This has the same problems that array implementations of traits without const generics had:
- It requires lots of awkward boilerplate.
- It's unpleasant to code and maintain; compiler errors are a lot less readable than with regular generics.
- It only implements the trait up to a fixed number of fields, usually 12. That means that a user with a 13-uple is out of luck.
Variadic generics would provide an idiomatic way to implement a trait for arbitrary tuples.
The ecosystem has a few utility methods that operate on pairs of objects, such as Iterator::zip or async-std's Future::join.
There are often use cases where one might want to call these methods to combine more than two objects; currently, the default way to do so is to call the method multiple times and chain the results, eg a.join(b).join(c) which essentially returns Joined(Joined(a, b), c) (kind of like "cons lists" in Lisp).
A common workaround is instead implement the utility method as a macro, which can take an arbitrary number of parameters, but this isn't always convenient.
I have never seen this use case suggested before, but it seems like an obvious feature to me; it's also the main use case for variadics in D.
Rust has a lot of crates centered around providing macros to enhance your types. These macros often follow a pattern of "list your type's fields, and then for each of the fields, do something similar". For instance:
Rust has a large ecosystem dedicating to enhancing data structures with #[attribute]
#[derive(serde::Serialize, serde::Deserialize, Debug)]
struct Point {
x: i32,
y: i32,
}The Serialize, Deserialize and Debug macros all follow the same principle of "do something with x, then do something with y", where the "something" in question can be easily defined with traits. For each trait, this is done by generating a string of tokens that compiles to a trait implementation for Point.
By contrast, a serialization function in D will look like
void serialize_struct(T)(Writer writer, string name, const T value)
{
writer.startObject(name);
static foreach (memberName; __traits(allMembers, T))
{{
auto memberValue = __traits(getMember, value, member);
alias PlainMemberT = typeof(cast() memberValue);
static if (isStruct!PlainMemberT)
{
serialize_struct(writer, memberName, memberValue);
}
else
{
// Serialize leaf types (eg integers, strings, etc)
// ...
}
}}
}There are a lot of subtle differences between D's semantics and Rust's, which means some concepts can't be trivially ported; I personally think these differences, like much of D's generics, are under-studied, so this document won't go into details.
The gist of it is that D's generics are closer to Rust macros than Rust generics. They feel like a scripting language, where entire chunks of code can be disabled or reinterpreted based on template parameters; so naturally post-monomorphization errors are much more frequent.
That said, the enthusiastic adoption of static foreach shows that there is a strong demand for an easy-to-use, idiomatic feature to write code that applies to each field of a data structure.
Some variadic generics proposals mention other possible use cases. I think they are less compelling, so I'll cover them very briefly:
-
Fn traits: Fn traits currently work with compiler magic, so that they be called with arbitrary numbers and types of arguments. Proposals mention that implementing variadic tuples would offer more flexibility when working with higher-order functions, though I'm personally not sure how relevant that use case would be.
-
Tuple manipulation: Proposals mention that tuples could be enhanced to be flattened, or indexed with constant integers, like C++ tuples allow:
// In C++11 you can do: do_something(std::get<1>(tuple));
I think these kinds of features would be a lot more likely to bump into post-monomorphisation errors, and have limited real-world uses.
There's an interesting conversation about what we want Rust generics to be like; some of that conversation has already started, with the question of whether to allow maybe-panicking code in const generics. Whether to bring tuples closer to reified types, with flattening and indexing and so on, should be part of that conversation, which is broader than this document.
This section is not so much a proposal for variadic templates, so much as a list of various common considerations
General syntax Allow partial matches Avoid post-monomorphisation errors Be able to take multiple tuples Map tuples to other types
Allow square expansions? Eg ...(i32, u32) * ...(i8, u8) => (i32, i8), (i32, u8), (u32, i8), (u32, u8)
- Design elements
- What a MVP should look like
- What we shouldn't do immediately
- cons-list
- Allowing template to to indexing or arbitrary tuples
- Concatenating tuples
- Variadic functions
"Recursive" tuples (like C++ variadics)
"@ .." to reuse existing syntax
https://github.com/fredpointzero/rfcs/blob/variadic_tuples/text/0000-variadic-tuples.md https://github.com/alexanderlinne/rfcs/blob/variadic_generics/text/0000-variadic-generics.md https://github.com/memoryleak47/variadic-generics/blob/master/0000-variadic-generics.md rust-lang/rfcs#376
We have to distinguish between variadic functions and variadic types Generally speaking, existing languages implement variadic functions in one of three fashions:
- Have a special variadic argument at the end of a function. That variadic only handles a single type, and is more of a shorthand for passing a slice (ex: Java, Go, C#).
- Have a special, dynamically-type variadic argument at the end of a function. Usually in dynamically-type languages, this variadic handles different types, but these types can only be differentiated at runtime, with no compile-time validation (ex: C, Python, Javascript).
The thing I'm personally not too enthusiastic about is derives as a big motivation for this. If you wanted to able to inspect all parts of the type the derive was used on like proc-macro derives can, you would basically end up with
const Input: syn::DeriveInputinstead ofconst Name: Stringandconst FieldNames: [String]. The only improvement over proc-macros then would be the ability to declare some assumptions the derive macro has of the input (types of a struct's fields implement a certain trait, maybe some other things), and I would argue that most macros have more complex requirements of their input than that. I think without post-morphization errors, only <5% of what people want derives to do could be done with something like#[variadic_derive], which stands in stark contrast to the effort that would be required to enable it.The issue of proc-macro compile times is also being tackled from other sides (see watt and cargo-watt), which I find a lot more compelling.