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

Tuple-Based Variadic Generics #1935

Closed
wants to merge 22 commits into from
Closed
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 207 additions & 0 deletions text/0000-variadic-generics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
- Feature Name: `variadic_generics`
- Start Date: 2017-2-22
- RFC PR: (leave this empty)
- Rust Issue: (leave this empty)

# Summary
[summary]: #summary

This RFC proposes the addition of several features to support variadic generics:
- An intrinsic `Tuple` trait implemented exclusively by tuples
- `(Head, ...Tail)` syntax for tuple types where `Tail: Tuple`
- `let (head, ...tail) = tuple;` pattern-matching syntax for tuples
- `let tuple = (head, ...tail);` syntax for joining an element with a tuple

# Motivation
[motivation]: #motivation

Variadic generics are a powerful and useful tool commonly requested by Rust
users (see
[#376](/~https://github.com/rust-lang/rfcs/issues/376) and
[#1921](/~https://github.com/rust-lang/rfcs/pull/1921)). They allow
programmers to abstract over non-homogeneous collections, and they make it
possible to implement functions which accept arguments of varying length and
type.

Rust has a demonstrable need for variadic generics.

In Rust's own standard library, there are a number of traits which have
been repeatedly implemented for tuples of varying size up to length 12 using
macros. This approach has several downsides:
- It presents arbitrary restrictions on tuples of size 13+.
- It increases the size of the generated code, resulting in slow compile times.
- It complicates documentation
(see the list of trait implementations in
[this documentation](https://doc.rust-lang.org/std/primitive.tuple.html)).

These arbitrary tuple-length restrictions, manual tuple macros, and confusing
documentation all combine to increase Rust's learning curve.

Furthermore, community library authors are required to implement similar
macro-based approaches in order to implement traits for tuples. In the `Diesel`
crate, it was discovered that replacing macro-generated tuple implementations
with a structurally-recursive implementation (such as the one proposed here)
resulted in a 50% decrease in the amount of code generated and a 70% decrease
in compile times ([link](/~https://github.com/diesel-rs/diesel/pull/747)). This
demonstrates that Rust's lack of variadic generics is resulting in a subpar
edit-compile-debug cycle for at least one prominent, high-quality crate.

The solution proposed here would resolve the limitations above by making it
possible to implement traits for tuples of arbitrary length. This change would
make Rust libraries easier to understand and improve the edit-compile-debug
cycle when using variadic code.


# Detailed design
[design]: #detailed-design

## The `Tuple` Trait
The following would be implemented by all tuple types:
```rust
trait Tuple {
type AsRefs<'a>: Tuple + 'a;
type AsMuts<'a>: Tuple + 'a;
fn elements_as_refs<'a>(&'a self) -> Self::AsRefs<'a>;
fn elements_as_mut<'a>(&'a mut self) -> Self::AsMuts<'a>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to avoid a hard dependency on ATC here, and I'm not sure the functionality needs to be in Tuple itself.
IOW, we can do this more transparently to the user with a recursive impl perhaps? Well, no, "compiler knows best", it could project AsRefs partially (e.g. (A, ...B, C)::AsRefs == (&A, ...B::AsRefs, &C)), but we can probably experiment with a recursive impl in a library, for a trait TupleAsRefs<'a>, to start with.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear, it's likely we'll want to only stabilize the ATC form itself, for convenience's sake, but it's not needed to experiment IMO.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah-- I left a comment about this in the "unresolved questions" section. Separate traits (TupleAsRef<'a> and TupleAsMut<'b>) work and eliminate the dependency on ATCs, but I think it's slightly less clear to the end user.

}
```

The types `AsRefs` and `AsMuts` are the corresponding tuples of references to
each element in the original tuple. For example,
`(A, B, C)::AsRefs = (&A, &B, &C)` and
`(A, B, C)::AsMuts = (&mut A, &mut B, &mut C)`

The `Tuple` trait should only be implemented for tuples and marked with the
`#[fundamental]` attribute described in
[the coherence RFC](/~https://github.com/rust-lang/rfcs/blob/master/text/1023-rebalancing-coherence.md).
This would allow coherence and type-checking to be extended to assume that no
implementations of `Tuple` will be added. This enables an increased level of
negative reasoning making it easier to write blanket implementations of traits
for tuples.

## The `(Head, ...Tail)` Type Syntax
This syntax would allow for a `Cons`-cell-like representation of tuple types.
For example, `(A, ...(B, C))` would be equivalent to `(A, B, C)`. This allows
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is potentially misleading, as (A, ...(B, C), D) would also be valid. Any mention of "cons" is likely to confuse more than help, compared to, say, "tuple concatenation" (which is valid for both types and values). See #1921 (comment) and #1921 (comment).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had actually purposefully avoided discussing things like (...(A, B,) C) (tuples in non-tail position). I wasn't sure if that was something we wanted to support in the initial implementation, but I think it makes sense to include them prior to stabilizing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sgrif thought too (#1921 (comment)) but I disagree, from the implementation side doing it right the first time is not much harder, and it's less work overall than having two incompatible implementations at different points in time.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know-- you have a much better sense of the actual implementation then I do. I'll edit the RFC appropriately.

Copy link
Member

@eddyb eddyb Feb 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be more clear: both tail-only and generalized single-repeat require the same primitive: a single check somewhere in the compiler that the T in ...T is now known to be a tuple (i.e. after substituting in type parameters, resolving associated types, etc.) therefore expand it.
And concatenating 3 lists of types is hardly any more difficult than concatenating 2 lists.
This is done only once, mind you (probably should be done when creating any tuple IMO).

On top of that, everywhere a tuple is introspected now has to take into account a partially unknown tuple, and that has the same cost for both approaches, except if you do it one way then later you have to redo in the other way, and there can even be subtle bugs introduced.

users to represent the type of tuples in an inductive style when writing trait
implementations.

## The `(head, ...tail)` Pattern-Matching Syntax
This syntax allows for splitting apart the head and tail of a tuple. For
example, `let (head, ...tail) = (1, 2, 3);` moves the head value, `1`, into
`head`, and the tail value, `(2, 3)`, into `tail`.

## The `(head, ...tail)` Joining Syntax
This syntax allows pushing an element onto a tuple. It is the natural inverse
of the pattern-matching operation above. For example,
`let tuple = (1, ...(2, 3));` would result in `tuple` having a value of
`(1, 2, 3)`.

## An Example

Using the tools defined above, it is possible to implement `TupleMap`, a
trait which can apply a mapping function over all elements of a tuple:

```rust
trait TupleMap<F>: Tuple {
type Out: Tuple;
fn map(self, f: F) -> Self::Out;
}

impl<F> TupleMap<F> for () {
type Out = ();
fn map(self, _: F) {}
}

impl<Head, Tail, F, R> TupleMap<F> for (Head, ...Tail)
where
F: Fn(Head) -> R,
Tail: TupleMap<F>,
{
type Out = (R, ...<Tail as TupleMap<F>>::Out);

fn map(self, f: F) -> Self::Out {
let (head, ...tail) = self;
(f(head), ...tail.map(f))
}
}
```

This example is derived from
[a playground example by @eddyb](https://play.rust-lang.org/?gist=8fd29c83271f3e8744a3f618786ca1de&version=nightly&backtrace=0)
that provided inspiration for this RFC.

The example demonstrates the concise, expressive code enabled
by this RFC. In order to implement a trait for tuples of any length, all
that was necessary was to implement the trait for `()` and `(Head, ...Tail)`.

# How We Teach This
[teach]: #teach

The `(head, ...tail)` and `(Head, ...Tail)` syntax closely mirror established
patterns for working with `Cons`-cell based lists. Rustaceans coming from
other functional programming languages will likely be familiar with the concept
of recursively-defined lists. For those unfamiliar with `Cons`-based
lists, the concept should be introduced using "structural recursion": there's
a base case, `()`, and a recursive/inductive case: `(Head, ...Tail)`. Any tuple
can be thought of in this way
(for example, `(A, B, C)` is equivalent to `(A, ...(B, ...(C, ...())))`).
Copy link
Member

@eddyb eddyb Feb 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


The exact mechanisms used to teach this should be determined after getting more
experience with how Rustaceans learn. After all, Rust users are a diverse crowd,
so the "best" way to teach one person might not work as well for another. There
will need to be some investigation into which explanations are more
suitable to a general audience.

As for the `(head, ...tail)` joining syntax, this should be explained as
taking each part of the tail (e.g. `(2, 3, 4)`) and inlining or un-"tupling"
them (e.g. `2, 3, 4`). This is nicely symmetrical with the `(head, ...tail)`
pattern-matching syntax.

The `Tuple` trait is a bit of an oddity. It is probably best not to go too
far into the weeds when explaining it to new users. The extra coherence
benefits will likely go unnoticed by new users, as they allow for more
advanced features and wouldn't result in an error where one didn't exist
before. The obvious exception is when trying to implement the `Tuple` trait.
Attempts to implement `Tuple` should resort in a relevant error message,
such as "The `Tuple` trait cannot be implemented for custom types."

# Drawbacks
[drawbacks]: #drawbacks

As with any additions to the language, this RFC would increase the number
of features present in Rust, potentially resulting increased complexity
of the language.

There is also some unfortunate overlap between the proposed `(head, ...tail)`
syntax and the current inclusive range syntax. However, the similarity
between `start...end` and `...tail` can be disambiguiated by whether or not
there is an expression immediately before the ellipsis.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry but ...x already is a valid inclusive range expression (it creates a std::ops::RangeToInclusive<Tail>). This code works today in nightly:

#![feature(inclusive_range_syntax)]
fn main() {
    println!("{:?}", (1, ...(2,)));
    // prints `(1, ...(2,))`
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you're correct. @eddyb made the same observation above.


# Alternatives
[alternatives]: #alternatives

- Do nothing.
- Implement one of the other variadic designs, such as
[#1582](/~https://github.com/rust-lang/rfcs/pull/1582) or
[#1921](/~https://github.com/rust-lang/rfcs/pull/1921)
- Include explicit `Head`, `Tail`, and `Cons<T>` associated types in the `Tuple`
trait. This could allow the above syntax to be implemented purely as sugar.
However, this approach introduces a lot of additional complexity. One of the
complications is that such a trait couldn't be implemented for `()`, so
there would have to be separate `Cons` and `Split` traits, rather than one
unified `Tuple`.

# Unresolved questions
[unresolved]: #unresolved-questions
-It might be useful in the future to expand on the locations where `...Type`
can be used. Potential extensions to this RFC could allow `...Type` in
non-tuple generics or in function argument types, like
`fn foo<Args>(args: ...Args)`.
This would allow functions and traits to use variadic generics without
explicit tuples. This could enable things like the proposed `foo[i, j]` syntax
using`Index<usize, usize>`.
-Should the `Tuple` trait use separate `TupleRef<'a>` and `TupleMut<'b>` traits
to avoid dependency on ATCs? It seems nicer to have them all together in one
trait, but it might not be worth the resulting feature-stacking mess.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overlap with range syntax is a problem. I believe x... could always work if we decide it doesn't make sense as a range, over x.. (i.e. is "inclusive at infinity" useful?).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

x... is how you expand a parameter pack in C++, so it'd be familiar to some, for better or worse.