-
Notifications
You must be signed in to change notification settings - Fork 60
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
interaction of tuple layout and variadic generics #105
Comments
The rule specified that let x = (1u8, 2u32, 3u8);
let (_, y @ ..) = &x;
// y is now `&(u32, u8)` Tail-borrowing is a pretty convenient tool which would simplify some problems raised by variadic generics. However, it would require that the representation of tuples must contain the representation of its tail. That is, Because we want to allow structs field order to be permuted arbitrarily, we don't want to specify that tuple representation and struct representations match, as this would prohibit tail-borrowing variadics. |
Leaving aside the question of whether the loss of layout optimizations for tuples is acceptable, can you elaborate on how tail borrowing helps with variadic generics? I'm aware of how it's used in rust-lang/rfcs#1582, but your rust-lang/rfcs#1935 instead proposed a conversion from reference-to-tuple to tuple-of-references-to-elements which (as the RFC itself notes) avoids the unfortunate layout implications of tail borrowing, while (AFAICT) providing the same functionality in the end. Is there anything insufficient about that approach? |
@rkruppe The unfortunate thing about that approach is that you always wind up with two trait impls-- one for a reference of tuples which delegates to a tuple of references. It works, but it's vaguely unergonomic. I'm not meaning to express a preference for one approach over another in this thread-- I just wanted to point out that there are valid reasons we might want to avoid specifying the layout of tuples as matching the layout of structs, and that overspecifying at this point could constrain us in future lang design choices. |
@rkruppe Short version (to avoid rehashing the entire RFC here) is that you either end up with coherence issues, poor ergonomics, or the need for additional complex features like mapping over a tuple and generic closures. It also would require ATCs no matter what. Ideally we'd write: impl Clone for () {
fn clone(&self) -> Self {
()
}
}
// postfix ... unpacks, prefix collects. `Tail` with no `...` is the tuple of the tail elements
impl<Head, Tail...> Clone for (Head, Tail...)
where
Head: Clone,
Tail: Clone,
{
fn clone(&self) -> Self {
let (ref head, ref ...tail) = *self;
(head.clone(), tail.clone()...)
}
} instead of /// postfix ... unpacks, is only valid where `T: Tuple`
impl<Head, Tail> Clone for (Head, Tail...)
where
Head: Clone,
Tail: Tuple,
for<'a> Tail::AsRefs<'a>: TupleClone<Output=Tail>,
{
fn clone(&self) -> Self {
let (head, ...tail) = self.as_refs();
(head.clone, tail.tuple_clone()...)
}
}
pub trait TupleClone {
type Output;
fn tuple_clone(self) -> Self::Output;
}
impl TupleClone for () {
type Output = Self;
fn tuple_clone(self) -> Self::Output {
()
}
}
impl<'a, Head, Tail> TupleClone for (&'a Head, Tail...)
where
Head: Clone,
Tail: Tuple + TupleClone,
{
type Output = (Head, Tail::Output...);
fn tuple_clone(self) -> Self::Clone {
let (head, ...tail) = self;
(head.clone(), tail.tuple_clone()...)
}
} There was some discussion at the all hands about a possible third option here where /// `...` *always* means "expand this for every element"
impl<Elems...> Clone for (Elems,...)
where
Elems: Clone,...
{
fn clone(&self) -> Self {
let (ref elems...) = *self;
(elems.clone(),...)
}
} |
I don't think it's obvious that it's worth making |
In my experience with C++, those "pack expansions" are annoyingly limiting / non-general:
Pack expansions in C++ also feel inconsistent with the rest of the language: they're intuitively a kind of iteration, but they look completely different from any of the kinds of iteration you can do at runtime. In my opinion, this outweighs the positives of what's admittedly a neat-looking syntax. For these reasons, for Rust, I'd personally much prefer an approach based on things like |
I'm fairly certain any tuple layout that allows tail references would be really inconvenient, and bad. It would require a recursive layout that ends up not only making things like I also don't think your point stands up to practice, @comex - the expansion syntax in C++ is simply pattern matching on lists, with some syntax sugar sprinkled on top, which I don't think is unreasonable. I personally prefer to have parameter packs with the list interface, rather than the iterable interface. |
I agree that tail references are a bad idea. I think a good solution would be to have magic elems.as_refs().map(|e| e.clone()) I don't know what you mean by "list interface". At the risk of stating the obvious, an important point about the C++ syntax is that parameter packs and tuples have to be two different things. For this to work: (elems.clone(),...) the compiler needs to know that inside the expression (elems + x,...) where In C++, this is a big limitation because there is no way to go from a tuple to a parameter pack "in place", as opposed to calling something else which then pattern matches to get a parameter pack. In the proposed Rust syntax, there is such a way – for expressions: let (ref elems...) = *self; (Incidentally, there's a proposal to allow similar syntax in C++ to introduce parameter packs.) Still, there might be cases where you start with a tuple-typed variable, then need to write a second name for a parameter-pack version: let my_tuple = get_a_tuple();
do_something_with(&my_tuple);
let (my_tuple_pack...) = my_tuple; In that example, More importantly, what about types? Presumably the expansion syntax would also work on tuple types, so you could write struct X<T>;
struct Foo<(Ts...)> {
xs: (X<Ts>...)
} But if you have a type 'expression' (say, Admittedly, the alternative might be that instead of only being able to use expansion syntax sometimes, you'd never be able to use it. We don't have type lambdas, so there can't be a convenient type version of By the way, another issue with the syntax is when the intended replacement doesn't contain the variable being iterated on. If you want to replace every element of an unknown-sized tuple with 0, so, e.g., you'd go from let (elems...) = expr;
let zeroes = ({&elems; 0}...); but that's super ugly. With let zeroes = elems.map(|_| 0); |
I tend to agree. On one hand, pack expansion in C++ is a nice primitive because it easily allows implementing On the other hand, pack expansion has a significant compile-time cost in C++ and people do actually prefer a good list API in practice (e.g. range-v3 tuple_algorithm.hpp). A lot of effort has been invested into making these "list APIs" fast, from compiler intrinsics like the one exposed by Rust has tuples in the language, and these can be used to implement type-list and similar types (e.g. type-indexed hash tables, etc.). If we can come up with a list API that solves all use cases of pack expansion / fold-expression, while having good compile-times, we probably should do that independently of whether we decide to pursue pack expansion as well or not. A good list API might make pack expansion not worth pursuing, but it isn't an either list API or pack expansion, we could (for better or worse) add both. Also I wouldn't focus on fold-expressions too much. They are a nice ergonomic improvement, but they are not required per-se. Everything that they allow can be emulated with a bit of boiler plate on top of pack expansion, e.g., with pack-expansion one can implement an heterogeneous |
Let's not rehash that whole discussion here, please. Thank you @cramertj and @sgrif for explaining why the tail-reference approach is not obviously completely superseded by another approach. With that in mind and considering that there's nothing actually specified about struct layout yet that would spill over on tuples, I think we should decouple tuple layout from struct layout to resolve this concern for now. Hopefully we'll get around to nailing down the more contentious parts of struct layout at some point, and when we do that we can also revisit tuple layout, hopefully informed by progress made in the area of variadic generics since then. |
Yes, I agree that it's not obvious. I don't think any direction here is obvious. I'm pointing out that there's a decision here to be made and two options are in conflict, so we should hold off on guaranteeing something in order to avoid forcing our hand elsewhere. Is anyone blocked by not knowing whether tuples and tuple structs have the same representation? I'm having a hard time imagining a usecase for this, so it seems like an obvious choice to leave this unspecified for now until we've had a (separate!) discussion on how variadics should work. |
@cramertj Really looking forward to the discussion because concerns about variadic generics has been blocking the stablization of fn_traits for years. Really wish minimal decision could be made to at least unblock the stablization of |
@crlf0710 That's not even that tied to this, IMO. There's no reason we can't allow you writing this: impl FnOnce(A, B) for Foo {
type Output = X;
fn call_once(self, a: A, b: B) -> X {...}
} Without even changing the definition of the traits in I started cleaning up the implementation in this area but got side-tracked. Not sure if it would be easier to mentor someone, or finish it up myself, but I'd be happy to discuss this further (although here it's offtopic). |
@eddyb could you say more in rust-lang/rust |
This issue doesn't seem useful anymore as it's not about any extant variadic generics proposals. People with such proposals are welcome to open new issues with concrete questions. |
@cramertj mentioned in the lang-team sync meeting that they had some concerns about the rules we specified for tuple layout and their interaction with variadic generics. I'm not entirely sure what they had in mind, but I'm opening this issue so that we track it down and figure it out.
(Also, I believe that @sgrif was mentioned as having an interest, and of course I know @eddyb cares about this -- so cc y'all.)
The text was updated successfully, but these errors were encountered: