rfcs
rfcs copied to clipboard
Draft RFC: variadic generics
Issue by eddyb
Monday Oct 28, 2013 at 17:39 GMT
For earlier discussion, see https://github.com/rust-lang/rust/issues/10124
This issue was labelled with: B-RFC in the Rust repository
The Problem
- fn types need a reform, and being able to define a trait with a variable number of type parameters would help
- working with functions which have a variable number of arguments is impossible right now (e.g. a generic
bind
method,f.bind(a, b)(c) == f(a, b, c)
) and defining such functions may only be done (in a limited fashion) with macros - tuples also have similar restrictions right now, there is no way to define a function which takes any tuple and returns the first element, for example
The Solution: Part One
C++11 sets a decent precedent, with its variadic templates, which can be used to define type-safe variadic functions, among other things.
I propose a similar syntax, a trailing ..T
in generic formal type parameters:
type Tuple<..T> = (..T); // the parens here are the same as the ones around a tuple.
// use => expansion
Tuple<> => ()
Tuple<int> => (int,)
Tuple<A, B, C> => (A, B, C)
The simple example above only uses ..T
, but not T
itself.
The question which arises is this: what is T
? C++11 has a special case for variadic parameter packs, but we can do better.
We have tuples. We can use them to store the actual variadic generic type parameters:
// Completely equivalent definition:
type Tuple<..T> = T;
// Detailed expansion of (..T) from above:
Tuple<> => (..()) => ()
Tuple<int> => (..(int,)) => (int,)
Tuple<A, B, C> => (..(A, B, C)) => (A, B, C)
The Solution: Part Two
Now that we know the answer is "tuples", everything else is about extending them.
The prefix ..
operator would expand a tuple type:
// in another tuple type:
type AppendTuple<T, U> = (..T, U);
type PrependTuple<T, U> = (U, ..T);
AppendTuple<PrependTuple<Tuple<B>, A>, C> => (A, B, C)
// in a fn type's parameter list:
type VoidFn<..Args> = fn(..Args);
VoidFn<A, B, C> => fn(A, B, C);
// in a variadic generic parameter list:
Tuple<..(A, B, C), ..(D, E, F)> => (A, B, C, D, E, F)
Then we can do the same thing with values:
// in another tuple:
(..(true, false), ..(1, "foo"), "bar") => (true, false, 1, "foo", "bar")
// in a function call:
f(..(a, b), ..(c, d, e), f) => f(a, b, c, d, e, f)
There's only one piece missing: we're still not able to define a function which takes a variable number of arguments.
For this, I propose ..x: T
(where T is a tuple type) in a pattern, which can be used to "capture" multiple arguments when used in fn formal arguments:
// Destructure a tuple.
let (head, ..tail) = foo;
// This function has the type fn(int, A, B, C), and can be called as such:
fn bar(_: int, ..x: (A, B, C)) -> R {...}
A type bound for ..T
(i.e. impl<..T: Trait>
) could mean that the tuple T
has to satisfy the bound (or each type in T
, but that's generally less useful).
Examples:
// The highly anticipated Fn trait.
trait Fn<Ret, ..Args> {
fn call(self, .._: Args) -> Ret;
}
impl Fn<Ret, ..Args> for fn(..Args) -> Ret {
fn call(self, ..args: Args) -> Ret {
self(..args)
}
}
impl Fn<Ret, ..Args> for |..Args| -> Ret {
fn call(self, ..args: Args) -> Ret {
self(..args)
}
}
// Cloning tuples (all cons-list-like algorithms can be implemented this way).
impl<Head, ..Tail: Clone> Clone for (Head, ..Tail) {
fn clone(&self @ &(ref head, ..ref tail)) -> (Head, ..Tail) {
(head.clone(), ..tail.clone())
}
}
impl Clone for () {
fn clone(&self) -> () {
()
}
}
// Restricting all types in a variadic tuple to one type.
trait ArrayLikeTuple<T> {
fn len() -> uint;
}
impl<T> ArrayLikeTuple<T> for () {
fn len() -> uint {0}
}
impl<T, ..Tail: ArrayLikeTuple<T>> ArrayLikeTuple<T> for (T, ..Tail) {
fn len() -> uint {
1 + Tail::len()
}
}
// And using that trait to write variadic container constructors.
impl<T> Vec<T> {
fn new<..Args: ArrayLikeTuple<T>>(..args: Args) -> Vec<T> {
let v: Vec<T> = Vec::with_capacity(Args::len());
// Use an unsafe move_iter-like pattern to move all the args
// directly into the newly allocated vector.
// Alternatively, implement &move [T] and move_iter on that.
}
}
// Zipping tuples, using a `Tuple` kind and associated items.
trait TupleZip<Other>: Tuple {
type Result: Tuple;
fn zip(self, other: Other) -> Result;
}
impl TupleZip<()> for () {
type Result = ();
fn zip(self, _: ()) -> () {}
}
impl<A, ATail: TupleZip<BTail>, B, BTail: Tuple> TupleZip<(B, ..BTail)> for (A, ..ATail) {
type Result = ((A, B), ..ATail::Result);
fn zip(self @ (a, ..a_tail), (b, ..b_tail): (B, ..BTail)) -> Result {
((a, b), ..a_tail.zip(b_tail))
}
}
Todo
- better formal descriptions
- figure out the details of pattern matching
- more examples
There is one issue with my initial approach: a reference to a "subtuple" of a larger tuple could have different padding than the tuple type made out of those fields.
One solution I may have is a &(ref head, ..ref tail)
pattern turning &(A, B, C, D)
into &A
and (&B, &C, &D)
, i.e. a subtuple of references instead of a reference of a subtuple.
But I have no idea how inference or generics would work with it.
This is probably a prerequisite for taking the Cartesian product of iterators without a macro, like in rust-itertools/iproduct! and bluss/rust-itertools#10.
This should be expanded to also support using the ".." operator on fixed size arrays (behaves equivalent to a tuple where the elements are all the same type and the arity matches the length of the array).
In combination with https://github.com/rust-lang/rfcs/pull/174 this would make it much easier to deal with variadic arguments of the same type. Something which even C++ is still missing.
This would need to be compatible with range syntax.
How would one implement the cartesian product of two type tuples with this?
@gnzlbg what exactly do you have in mind, Product<(A, B), (X, Y)> = ((A, X), (A, Y), (B, X), (B, Y))
?
@eddyb yes, but with variadics.
Having wrestled with c++11 templates and Rust macros, I really like this RFC. I hope it comes to fruition sooner rather than later.
Me too, I really wished this to come before 1.0 and solved tuple impl etc.
Unfortunately this syntax doesn't work, because the ..
operator is already taken by range syntax. We could avoid this problem by using triple dots like C++. Which are also already taken (by inclusive range syntax), but only as infix operator and only in pattern syntax (though there were some requests to allow triple dots in expressions, too).
For the general idea: :+1:, I was imagining variadic generics using pretty much the same approach. However, the devil is in the details that aren't yet worked out. C++ resolves expressions involving parameter pack expansion only after monomorphization; but I assume Rust would want to type-check the generic code? Wouldn't that require restrictions on where the unpacking operator is used? Also, consuming variadics in C++ usually involves a recursive function using template specialization. With this proposal, it seems like you'd need to create a trait for every variadic function, so that you can "specialize" using trait impls as in the Clone
example?
Also, the ...
operator in C++ allows "unpacking" complex expressions, not just directly unpacking the parameter pack. For example, in C++11 I can do:
template <typename... T> void consumer(T... t) { ... }
template <typename... T> void adapter(T... t)
{
consumer(t.adapt()...);
// expands to:
// consumer(t1.adapt(), t2.adapt(), t3.adapt(), ...);
}
What's the easiest way to write the 3-line adapter
function in Rust with this RFC? Do I have to write recursive Adapt
implementations like the Clone
impls in the example?
As for Vec::new
: I think in cases where we don't actually need a type parameter pack, but just a variadic function with a single parameter type, it might be better to support something like this:
fn new(..elements: [T]) -> Vec<T> { ... }
This requires being able to pass unsized types as parameters - but that seems doable (and might already be planned?).
Why not the dereference operator?
type AppendTuple<T, U> = (*T, U);
f(*(a, b), *(c, d, e), f) => f(a, b, c, d, e, f);
@dgrunwald :
Also, consuming variadics in C++ usually involves a recursive function using template specialization.
In C++1y there are fold-expressions that allow you to do a lot of things without recursion:
template<typename... Args>
bool all(Args... args) { return (... && args); }
bool b = all(true, true, true, false);
but I assume Rust would want to type-check the generic code? Wouldn't that require restrictions on where the unpacking operator is used?
You can easily type check that all parameters in the parameter pack implement a given trait. If one needs more complicated type-checking a variadic function is likely not the best solution.
@gnzlbg that's C++1z actually.
The syntax between the value-level and type-level languages should be kept as close as possible. Already the +
operator means the intersection of traits at the type level, even though *
(or &
) more accurately conveys a lattice meet.
@gnzlbg I don't think that form of duck-typing is compatible with Rust's trait-based generics.
Rather than having special fold syntax, it's cleaner to design some system compatible with HKTs for constraining the tuple's types, and then iterate over a tuple for fold
any other operation:
fn fold_variadic<*T> (x: T) -> u32
where *T: BitAnd<u32>
{
x.iter().fold(!0u32, |b, a| b & a)
}
fold_variadic(0xFFF0, 0x0FFF) == 0x0FF0
In the above example, T is a tuple of variadic length, and *
is a currying operator. This makes intuitive sense once you realize that type arguments are effectively pattern-matched in variadic generics. Consider the partial signature fold_variadic::<*T>
and the invocation fold_variadic::<u32, u32, u32>
, and notice how T
is bound:
<*T> = <u32, u32, u32>
*T = u32, u32, u32
The curried/destructured tuple is matched to u32, u32, u32
, so the tuple is (u32, u32, u32)
.
The interaction of a curried tuple with +
, =
, and :
, as below, should also be specified.
where *T: BitAnd<u32>
// or
where *T = u32
A sensible semantic is distribution over the curried form. *T: BitAnd<u32>
means every type in the tuple implements BitAnd<u32>
, *T = u32
means every type in the (variable-length) tuple is u32
.
@oblitum you are correct. @alexchandel my previous post was just to show that in future C++ you won't need template specialization to solve this problems in c++.
A recurring task in C++ meta-programming is zipping a tuple of values with a tuple of types (tags, or std::integral_constant
s). I wonder how this could be done in Rust.
Any update on this?
@Vikaton +1 I would be really interested to know as well. This is the only feature that I miss from C++.
How will this interact with type level numerals (in particular, polymorphism over arrays of size N)?
There's been lots more discussion on this over on #1582 but I'll try to summarise here.
The main problem with all of these RFCs is the problem of memory layout for tuples. In general, in current Rust, a tuple of the form (A, B, C)
does not contain a tuple of the form (B, C)
anywhere in its representation. This means if you have an (A, B, C)
you can't take a reference to the "sub-tuple" (B, C)
because it doesn't exist. For example, if (A, B, C)
is the tuple (u16, u16, u32)
it may be represented as:
------------------------------------------------
| Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|------|---------|---------|-------------------|
| Data | A (u16) | B (u16) | C (u32) |
------------------------------------------------
By comparison, a tuple (B, C) == (u16, u32)
may be represented as.
------------------------------------------------
| Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|------|---------|---------|-------------------|
| Data | B (u16) | padding | C (u32) |
------------------------------------------------
Note the different position of B
.
There are two proposed solutions to this:
- Don't allow references to "sub-tuples" of tuples. Instead, when binding part of a tuple by reference, bind it as a tuple of references. This solves the problem but has the potential to be confusing. Under this scheme
let (head, ...tail) = (0, 1, 2)
will give youhead == 0, tail == (1, 2)
butlet (ref head, ...ref tail) = (0, 1, 2)
will give youhead == &0, tail == (&1, &2)
rather thantail == &(1, 2)
. - Only allow expanding tuples into the tail (or alternatively the prefix) of a larger tuple. This means that
(a, b, c, ...d)
would be valid but using...
anywhere else would not. Eg. these would not be valid:(a, b, ...c, d)
,(...a, b, c, d)
,(a, b, ...c, ...d)
etc. This still allows you to define anything you could without this restriction, it just becomes a lot more cumbersome for some usecases. Eg. instead of being able to write(...a, b)
you'd have to write this. Additionally, this approach requires that one of two changes are made to tuple layouts. The first option is to pack tuples less space-efficiently so that(A, B, C)
always contains a(B, C)
. For example(u16, u16, u32)
would have to be packed as
--------------------------------------------------------------------
| Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|------|---------|---------|---------|---------|--------------------
| Data | A (u16) | padding | B (u16) | padding | C (u32) |
--------------------------------------------------------------------
The second option is to accept RFC #1397 and reverse the layout order of tuples so that (u16, u16, u32)
would look like
------------------------------------------------
| Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|------|-------------------|---------|---------|
| Data | C (u32) | B (u16) | A (u16) |
------------------------------------------------
And its tail (u16, u32)
would look like
------------------------------------------------
| Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|------|-------------------|---------|---------|
| Data | C (u32) | B (u16) | padding |
------------------------------------------------
This comes at the cost of disallowing the compiler from generating code which overwrites padding at the end of tuples as the "padding" may actually contain the next element of a larger tuple.
IIUC what you want is to be able to slice (A,B,C)
into (B,C)
. I have some questions:
- Why/when isn't
(&B, &C)
enough? Is there a real use case for this? We cannot do this for structs, so why should we be able to do this for tuples? - Why does the language have to define the layout of tuples? I would rather have it be "implementation defined" so that the implementation can reorder the elements of a tuple to minimize padding. Some C++ implementations of tuple actually do this [*]. We can, like for structs, use
#[repr(C)]
to get a specific layout.
I think it would be great to be able to slice (A,B,C)
into (B,C)
and still have (A,B,C)
with a perfectly packed memory layout but I don't see how we could achieve both things.
[*]: https://rmf.io/cxx11/optimal-tuple-i/, https://rmf.io/cxx11/optimal-tuple-ii/, https://rmf.io/cxx11/optimal-tuple-iii/ , https://rmf.io/cxx11/optimal-tuple-iv/
Meaning if the trade-off is between sugar for a particular use case (slicing (A,B,C)
into (B,C)
instead of just (&B,&C)
) or a zero-cost abstraction (a perfectly packed tuple) I would chose the zero-cost abstraction any day.
FWIW the last option mentioned by @canndrew, involving https://github.com/rust-lang/rfcs/issues/1397, seems like clearly the cleanest and nicest design to me. The only big question mark hanging over it in my mind is the backwards (in)compatibility impact of https://github.com/rust-lang/rfcs/issues/1397 on already-existing unsafe
code that uses size_of
where only stride would now be correct.
I want to crosslink you to the fresh RFC clarification issue rust-lang/rfcs/pull/1592 because the commitment to having a specific position in the tuple possible to be unsized is relevant to the tuple representation discussion here.
I don't think variadic generics should care about layouts much based on some observations from C++ :
-
A lot of operations with variadics happen at the type level, where layout doesn't matter. E.g. tuple types
(A, B, C)
can be sliced, combined and transformed regardless of their layouts or representability. In this sense https://github.com/rust-lang/rfcs/pull/1592 seems unfortunate - it means you can't represent an arbitrary type list with a tuple type. -
The main use case for variadics - function arguments - don't need any layout guarantees. So function arguments can be represented with tuples at the type level, but they don't need to be kept as a tuple in memory. In this sense https://github.com/rust-lang/rfcs/pull/1592 seems unfortunate again, because with expected ability to pass unsized types to functions by value a function
fn([u8], [u8])
will be well formed, but its argument list will not be representable as a tuple type([u8], [u8])
. -
In C++ parameter packs (aka type lists, aka Rust tuple types) are type level constructs, but they can be materialized into values with two main instruments - tuples
std::tuple<Types...>
and closures with explicit captures[values...]{}
. Both std::tuples and anonymous closure structs have unspecified layouts, but for some reason it seems to not bother anyone, because taking sub-tuples by references doesn't seem to be especially useful. As for tuples of references suggested above, there's even a standard facilitystd::forward_as_tuple
materializing a tuple value(&A, &B, &C)
from any value list, not necessarily being a tuple itself.
Sorry for random thoughts, this really needs a more thorough investigation with examples from something like https://github.com/boostorg/hana
std::forward_as_tuple
is pretty much the original solution I had in mind for the reference problem.
I agree that not being able to use tuples as type lists because of unsized types is a problem.
cc @arielb1 @nikomatsakis
On the other hand, variadics should work with lifetime lists too and tuples can't represent them unless some transformation, like ('a, 'b, 'c) <=> (&'a (), &'b (), &'c ())
, is built into the language.
So maybe it still may make sense to make variadic lists separate tuple-like entities and not tuples, and leave tuples alone with their layout and representability problems. It's probably one of the most important choices to make here.
@petrochenkov I agree with all of those premises (re: the earlier comment), but differ about the conclusion.
C++ variadic generics (as is C++'s wont) are both very powerful and... very ugly. Lifting the high-level design for this kind of advanced feature from C++ simply because it's the only prior art we've familiarized ourselves with would make me kind of sad. Figuring out the right variadics generics design for Rust, so that it's as powerful as we need it to be but also integrates better with our type system, needs research and careful design work.
On the other hand, #1582 seems like a quite simple and fairly "obvious" solution for the narrower use case of just working with tuples of different arity, modulo the questions it raises about tuples' representation, and would solve many of the most common use cases for variadic generics as well. I can't really think of any other way it could sensibly be designed, nor any major reason we would want to. Even if or when we later implement "full" variadic generics, I don't think we'd really want it to be any different.
So while my feeling about variadic generics is "we need to approach this very carefully", my feeling about a design for tuple-destructuring like #1582 is "cool, let's do it, why not!".
(Again, as long as we can settle on a satisfactory solution for the representation issue...)
@glaebhoerl As long as we punt on taking a reference to a sub-tuple, the representation issue is a non-issue IMO. Also, if you've seen my prototype you'll note that the compiler support needed is pretty small.
I expect splitting operations to be done with associated types, at least initially, but spreading a tuple type in a type list (or a tuple value in a value list) can be there from the start.
I'd like to add the necessary infrastructure even if only to desugar the "rust-call"
madness into something that MIR can actually optimize.
@glaebhoerl
C++ variadic generics (as is C++'s wont) are both very powerful and... very ugly. Lifting the high-level design for this kind of advanced feature from C++ simply because it's the only prior art we've familiarized ourselves with would make me kind of sad.
I certainly wouldn't like to copy the design from C++ as is, it has its own problems - parameter packs are not first class enough, they cannot be indexed, sliced or aliased, everything has to be done through recursion which is ugly/inconvenient and causes long compile times (Clang even had to introduce special intrinsics to reduce compile times for std::tuple and friends), all these problems can be solved by making packs similar to Rust's built-in tuples, but C++ doesn't have built-in tuples.
We're conflating two different concepts here when talking about using tuples to represent lists of types. There's "tuple types", and then there's "a tuple of types". It may be that (str, str)
is not a valid tuple type, but it's a valid tuple of types.
To make this clearer, imagine we're in a dependently typed language where types can be used as values and have a type themselves (the type type
). Here you'd have two different things that could be ambiguously written as (u32, u32)
:
- The tuple type
(u32, u32)
where(0u32, 1u32) : (u32, u32) : type
- The tuple of types
(u32, u32)
where(u32, u32) : (type, type) : type
It's an abuse of tuple types to try and also use them as tuples of types.
Edit: And this makes me think we might need HKT in order to have some kind of kind-level-tuples in order to do variadic generics properly. I dunno what they would look like though.