Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft RFC: variadic generics #376

Open
rust-highfive opened this issue Oct 8, 2014 · 67 comments
Open

Draft RFC: variadic generics #376

rust-highfive opened this issue Oct 8, 2014 · 67 comments
Labels
A-typesystem Type system related proposals & ideas T-lang Relevant to the language team, which will review and decide on the RFC.

Comments

@rust-highfive
Copy link

Issue by eddyb
Monday Oct 28, 2013 at 17:39 GMT

For earlier discussion, see rust-lang/rust#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
@aturon aturon changed the title [CLOSED] RFC: variadic generics Draft RFC: variadic generics Oct 8, 2014
@eddyb
Copy link
Member

eddyb commented Oct 9, 2014

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.

@alexchandel
Copy link

This is probably a prerequisite for taking the Cartesian product of iterators without a macro, like in rust-itertools/iproduct! and rust-itertools/itertools#10.

@Diggsey
Copy link
Contributor

Diggsey commented Jan 24, 2015

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 #174 this would make it much easier to deal with variadic arguments of the same type. Something which even C++ is still missing.

@alexchandel
Copy link

This would need to be compatible with range syntax.

@gnzlbg
Copy link
Contributor

gnzlbg commented Feb 27, 2015

How would one implement the cartesian product of two type tuples with this?

@eddyb
Copy link
Member

eddyb commented Feb 27, 2015

@gnzlbg what exactly do you have in mind, Product<(A, B), (X, Y)> = ((A, X), (A, Y), (B, X), (B, Y))?

@gnzlbg
Copy link
Contributor

gnzlbg commented Feb 27, 2015

@eddyb yes, but with variadics.

@goertzenator
Copy link

Having wrestled with c++11 templates and Rust macros, I really like this RFC. I hope it comes to fruition sooner rather than later.

@oblitum
Copy link

oblitum commented Feb 27, 2015

Me too, I really wished this to come before 1.0 and solved tuple impl etc.

@dgrunwald
Copy link
Contributor

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: 👍, 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?).

@alexchandel
Copy link

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);

@gnzlbg
Copy link
Contributor

gnzlbg commented Mar 3, 2015

@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.

@oblitum
Copy link

oblitum commented Mar 4, 2015

@gnzlbg that's C++1z actually.

@alexchandel
Copy link

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.

@gnzlbg
Copy link
Contributor

gnzlbg commented Mar 5, 2015

@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_constants). I wonder how this could be done in Rust.

@5nyper
Copy link

5nyper commented Dec 22, 2015

Any update on this?

@semirix
Copy link

semirix commented Dec 24, 2015

@Vikaton +1 I would be really interested to know as well. This is the only feature that I miss from C++.

@ticki
Copy link
Contributor

ticki commented Jan 9, 2016

How will this interact with type level numerals (in particular, polymorphism over arrays of size N)?

@canndrew
Copy link
Contributor

canndrew commented Apr 23, 2016

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 you head == 0, tail == (1, 2) but let (ref head, ...ref tail) = (0, 1, 2) will give you head == &0, tail == (&1, &2) rather than tail == &(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.

@gnzlbg
Copy link
Contributor

gnzlbg commented Apr 25, 2016

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/

@gnzlbg
Copy link
Contributor

gnzlbg commented Apr 25, 2016

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.

@glaebhoerl
Copy link
Contributor

FWIW the last option mentioned by @canndrew, involving #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 #1397 on already-existing unsafe code that uses size_of where only stride would now be correct.

@bluss
Copy link
Member

bluss commented Apr 27, 2016

I want to crosslink you to the fresh RFC clarification issue /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.

@qraynaud
Copy link

qraynaud commented Dec 7, 2017

@tinyplasticgreyknight : even so, every function would have a different tuple type and that would not work either for me right?

Each method for each parser rule's pattern has a different signature. I think that was not clear enough. What I implemented in C++ is similar to the apply method in JS (obj.method.apply(obj, [arg1, arg2, …])).

I don't really see how I can build the correct tuple, its signature being something like (..T) with each T being one type between a list of known types. With this idea I got my problem moved. Before it was calling a method using a vector of arguments, now it is constructing a tuple with a vector of arguments. Not knowing beforehand the types & the numbers of arguments…

@Centril Centril added the T-lang Relevant to the language team, which will review and decide on the RFC. label Dec 10, 2017
@takkuumi
Copy link

Will it implement in 2018?

@Centril
Copy link
Contributor

Centril commented Oct 7, 2018

@NateLing up for writing a formal RFC proposal about it? ;)

@takkuumi
Copy link

@Centril yeap

@Systemcluster
Copy link

For reference, since this seems to be the most prominent issue when looking for "variadic generics":

Since there is no dedicated tag for it, is there any place where the general progress of this topic can be followed?

@hirrolot
Copy link

hirrolot commented Aug 7, 2020

Why not just use induction on heterogeneous lists?
See: https://docs.rs/frunk/0.3.1/frunk/hlist/index.html

@ChayimFriedman2
Copy link

I just want to say here that now TypeScript 4.0 implemented the same thing in the same shape (more or less). See: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#variadic-tuple-types

@Kixiron
Copy link
Member

Kixiron commented Mar 29, 2021

This is a draft RFC, so I can pretty confidently say no

@burdges
Copy link

burdges commented Mar 29, 2021

If you want optional arguments soon then implement them on nightly using procmacros

fn foo(..mandatory_args..) -> Foo { Foo { ..mandatory_args.. } }
pub struct Foo { ..mandatory_args.. }
impl FnOnce<()> for Foo { .. }
impl FnOnce<OptArg1> for Foo { .. }
impl FnOnce<OptArg2> for Foo { .. }
impl FnOnce<OptArg3> for Foo { .. }
impl FnOnce<OptArg4> for Foo { .. }

and invoke like foo(mandatory_args)(optarg3)

It'd even support different return types for different optional argument types.

@Randl
Copy link

Randl commented Apr 1, 2021

Is anyone working on turning it into full RFC?
cc @eddyb @canndrew @Centril @NateLing

@eddyb
Copy link
Member

eddyb commented Apr 30, 2021

There's been several attempts over the years and it doesn't seem like it's going to happen again any time soon, sorry.

I have a branch somewhere with some of the implementation details (that can be used internally by rustc even without a proper VG feature, in order to clean up the extern "rust-call" mess), but even that I don't know when I'll get back to.

@86maid
Copy link

86maid commented Jan 30, 2024

Is there anyone else?

@jasal82
Copy link

jasal82 commented Apr 18, 2024

I don't think this will be implemented in 2018. It would still be pretty useful.

@kirawi
Copy link

kirawi commented Aug 23, 2024

@burdges
Copy link

burdges commented Aug 24, 2024

I'd expect some builder patters plus #3681 would be preferable for many if not most variadics use cases, ala MyFn { whatever args, .. }.go()

I suppose Iterator<Item = &dyn MyTrait> fits many of the remaining use cases, but if we'd some generic closure then maybe the compiler could derive a tuple trait from the base trait:

#[derive_tuple(MyTraitTuple)]
pub trait MyTrait { .. }

defines

trait MyTraitTuple {
    type Head: MyTrait;
    type Tail: MyTraitTuple;
    .. map fns using generic closures .. 
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-typesystem Type system related proposals & ideas T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

No branches or pull requests