6

Stabilize generic associated types by jackh726 · Pull Request #96709 · rust-lang...

 2 years ago
source link: https://github.com/rust-lang/rust/pull/96709
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Contributor

@jackh726 jackh726 commented on May 4

edited by nikomatsakis

Closes #44265

I'm going to spend the next couple days writing/working on external documentation, but I want this to open this to get FCPs started. If we're lucky, we can get this in 1.62 (if we're not, oh well).

r? @nikomatsakis

zap Status of the discussion zap

Stabilization proposal

This PR proposes the stabilization of #![feature(generic_associated_types)]. While there a number of future additions to be made and bugs to be fixed (both discussed below), properly doing these will require significant language design and will ultimately likely be backwards-compatible. Given the overwhelming desire to have some form of generic associated types (GATs) available on stable and the stability of the "simple" uses, stabilizing the current subset of GAT features is almost certainly the correct next step.

Tracking issue: #44265
Initiative: https://rust-lang.github.io/generic-associated-types-initiative/
RFC: https://github.com/rust-lang/rfcs/blob/master/text/1598-generic_associated_types.md
Version: 1.62 (2022-05-19 => beta, 2022-06-30 => stable).

[ ] Reference documentation
[ ] Book chapter

Motivation

There are a myriad of potential use cases for GATs. Stabilization unblocks probable future language features (e.g. async functions in traits), potential future standard library features (e.g. a LendingIterator or some form of Iterator with a lifetime generic), and a plethora of user use cases (some of which can be seen just by scrolling through the tracking issue and looking at all the issues linking to it).

There are a myriad of potential use cases for GATs. First, there are many users that have chosen to not use GATs primarily because they are not stable (some of which can be seen just by scrolling through the tracking issue and looking at all the issues linking to it). Second, while language feature desugaring isn't blocked on stabilization, it gives more confidence on using the feature. Likewise, library features like LendingIterator are not necessarily blocked on stabilization to be implemented unstably; however few, if any, public-facing APIs actually use unstable features.

This feature has a long history of design, discussion, and developement - the RFC was first introduced roughly 6 years ago. While there are still a number of features left to implement and bugs left to fix, it's clear that it's unlikely those will have backwards-incompatibility concerns. Additionally, the bugs that do exist do not strongly impede the most-common use cases.

What is stabilized

The primary language feature stabilized here is the ability to have generics on associated types, as so. Additionally, where clauses on associated types will now be accepted, regardless if the associated type is generic or not.

trait ATraitWithGATs {
    type Assoc<'a, T> where T: 'a;
}

trait ATraitWithoutGATs<'a, T> {
    type Assoc where T: 'a;
}

When adding an impl for a trait with generic associated types, the generics for the associated type are copied as well. Note that where clauses are allowed both after the specified type and before the equals sign; however, the latter is a warn-by-default deprecation.

struct X;
struct Y;

impl ATraitWithGATs for X {
    type Assoc<'a, T> = &'a T
      where T: 'a;
}
impl ATraitWithGATs for Y {
    type Assoc<'a, T>
      where T: 'a
    = &'a T;
}

To use a GAT in a function, generics are specified on the associated type, as if it was a struct or enum. GATs can also be specified in trait bounds:

fn accepts_gat<'a, T>(t: &'a T) -> T::Assoc<'a, T>
  where for<'x> T: ATraitWithGATs<Assoc<'a, T> = &'a T> {
    ...
}

GATs can also appear in trait methods. However, depending on how they are used, they may confer where clauses on the associated type definition. More information can be found here. Briefly, where clauses are required when those bounds can be proven in the methods that construct the GAT or other associated types that use the GAT in the trait. This allows impls to have maximum flexibility in the types defined for the associated type.

To take a relatively simple example:

trait Iterable {
    type Item<'a>;
    type Iterator<'a>: Iterator<Item = Self::Item<'a>>;

    fn iter<'x>(&'x self) -> Self::Iterator<'x>;
    //^ We know that `Self: 'a` for `Iterator<'a>`, so we require that bound on `Iterator`
    //  `Iterator` uses `Self::Item`, so we also require a `Self: 'a` on `Item` too
}

A couple well-explained examples are available in a previous blog post.

What isn't stabilized/implemented

Universal type/const quantification

Currently, you can write a bound like X: for<'a> Trait<Assoc<'a> = &'a ()>. However, you cannot currently write for<T> X: Trait<Assoc<T> = T> or for<const N> X: Trait<Assoc<N> = [usize; N]>.

Here is an example where this is needed:

trait Foo {}

trait Trait {
    type Assoc<F: Foo>;
}

trait Trait2: Sized {
    fn foo<F: Foo, T: Trait<Assoc<F> = F>>(_t: T);
}

In the above example, the caller must specify F, which is likely not what is desired.

Object-safe GATs

Unlike non-generic associated types, traits with GATs are not currently object-safe. In other words the following are not allowed:

trait Trait {
    type Assoc<'a>;
}

fn foo(t: &dyn for<'a> Trait<Assoc<'a> = &'a ()>) {}
         //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ not allowed

let ty: Box<dyn for<'a> Trait<Assoc<'a> = &'a ()>>;
          //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ not allowed

Higher-kinded types

You cannot write currently (and there are no current plans to implement this):

struct Struct<'a> {}

fn foo(s: for<'a> Struct<'a>) {}

Tests

There are many tests covering GATs that can be found in src/test/ui/generic-associated-types. Here, I'll list (in alphanumeric order) tests highlight some important behavior or contain important patterns.

  • ./parse/*: Parsing of GATs in traits and impls, and the trait path with GATs
  • ./collections-project-default.rs: Interaction with associated type defaults
  • ./collections.rs: The Collection pattern
  • ./const-generics-gat-in-trait-return-type-*.rs: Const parameters
  • ./constraint-assoc-type-suggestion.rs: Emit correct syntax in suggestion
  • ./cross-crate-bounds.rs: Ensure we handles bounds across crates the same
  • ./elided-in-expr-position.rs: Disallow lifetime elision in return position
  • ./gat-in-trait-path-undeclared-lifetime.rs: Ensure we error on undeclared lifetime in trait path
  • ./gat-in-trait-path.rs: Base trait path case
  • ./gat-trait-path-generic-type-arg.rs: Don't allow shadowing of parameters
  • ./gat-trait-path-parenthesised-args.rs: Don't allow paranthesized args in trait path
  • ./generic-associated-types-where.rs: Ensure that we require where clauses from trait to be met on impl
  • ./impl_bounds.rs: Check that the bounds on GATs in an impl are checked
  • ./issue-76826.rs: Windows pattern
  • ./issue-78113-lifetime-mismatch-dyn-trait-box.rs: Implicit 'static diagnostics
  • ./issue-84931.rs: Ensure that we have a where clause on GAT to ensure trait parameter lives long enough
  • ./issue-87258_a.rs: Unconstrained opaque type with TAITs
  • ./issue-87429-2.rs: Ensure we can use bound vars in the bounds
  • ./issue-87429-associated-type-default.rs: Ensure bounds hold with associated type defaults, for both trait and impl
  • ./issue-87429-specialization.rs: Check that bounds hold under specialization
  • ./issue-88595.rs: Under the outlives lint, we require a bound for both trait and GAT lifetime when trait lifetime is used in function
  • ./issue-90014.rs: Lifetime bounds are checked with TAITs
  • ./issue-91139.rs: Under migrate mode, but not NLL, we don't capture implied bounds from HRTB lifetimes used in a function and GATs
  • ./issue-91762.rs: We used to too eagerly pick param env candidates when normalizing with GATs. We now require explicit parameters specified.
  • ./issue-95305.rs: Disallow lifetime elision in trait paths
  • ./iterable.rs: Iterable pattern
  • ./method-unsatified-assoc-type-predicate.rs: Print predicates with GATs correctly in method resolve error
  • ./missing_lifetime_const.rs: Ensure we must specify lifetime args (not elidable)
  • ./missing-where-clause-on-trait.rs: Ensure we don't allow stricter bounds on impl than trait
  • ./parameter_number_and_kind_impl.rs: Ensure paramters on GAT in impl match GAT in trait
  • ./pointer_family.rs: PointerFamily pattern
  • ./projection-bound-cycle.rs: Don't allow invalid cycles to prove bounds
  • ./self-outlives-lint.rs: Ensures that an e.g. Self: 'a is written on the traits GAT if that bound can be implied from the GAT usage in the trait
  • ./shadowing.rs: Don't allow lifetime shadowing in params
  • ./streaming_iterator.rs: StreamingIterator(LendingIterator) pattern
  • ./trait-objects.rs: Disallow trait objects for traits with GATs
  • ./variance_constraints.rs: Require that GAT substs be invariant

Remaining bugs and open issues

A full list of remaining open issues can be found at: F-generic_associated_types `#![feature(generic_associated_types)]` a.k.a. GATs

There are some known-bug tests in-tree at src/test/ui/generic-associated-types/bugs.

Here I'll categorize most of those that GAT bugs (or involve a pattern found more with GATs), but not those that include GATs but not a GAT issue in and of itself. (I also won't include issues directly for things listed elsewhere here.)

Using the concrete type of a GAT instead of the projection type can give errors, since lifetimes are chosen to be early-bound vs late-bound.

Where clause bounds from associated types don't add implied bounds to functions. This means that using concrete types vs projection types can give unsatisfied lifetime bound errors.

In certain cases, we can run into cycle or overflow errors. This is more generally a problem with associated types.

Bounds on an associatd type need to be proven by an impl, but where clauses need to be proven by the usage. This can lead to confusion when users write one when they mean the other.

We sometimes can't normalize closure signatures fully. Really an asociated types issue, but might happen a bit more frequently with GATs, since more obvious place for HRTB lifetimes.

When calling a function, we assign types to parameters "too late", after we already try (and fail) to normalize projections. Another associated types issue that might pop up more with GATs.

We don't fully have implied bounds for lifetimes appearing in GAT trait paths, which can lead to unconstrained type errors.

Suggestion for adding lifetime bounds can suggest unhelpful fixes (T: 'a instead of Self: 'a), but the next compiler error after making the suggested change is helpful.

We can end up requiring that for<'a> I: 'a when we really want for<'a where I: 'a> I: 'a. This can leave unhelpful errors than effectively can't be satisfied unless I: 'static. Requires bigger changes and not only GATs.

Unlike with non-generic associated types, we don't eagerly normalize with param env candidates. This is intended behavior (for now), to avoid accidentaly stabilizing picking arbitrary impls.

Some Iterator adapter patterns (namely filter) require Polonius or unsafe to work.

Potential Future work

Universal type/const quantification

No work has been done to implement this. There are also some questions around implied bounds.

Object-safe GATs

The intention is to make traits with GATs object-safe. There are some design work to be done around well-formedness rules and general implementation.

GATified std lib types

It would be helpful to either introduce new std lib traits (like LendingIterator) or to modify existing ones (adding a 'a generic to Iterator::Item). There also a number of other candidates, like Index/IndexMut and Fn/FnMut/FnOnce.

Reduce the need for for<'a>

Seen here. One possible syntax:

trait Iterable {
    type Iter<'a>: Iterator<Item = Self::Item<'a>>;
}

fn foo<T>() where T: Iterable, T::Item<let 'a>: Display { } //note the `let`!

Better implied bounds on higher-ranked things

Currently if we have a type Item<'a> where self: 'a, and a for<'a> T: Iterator<Item<'a> = &'a (), this requires for<'a> Self: 'a. Really, we want for<'a where T: 'a> ...

There was some mentions of this all the back in the RFC thread here.

Alternatives

Make generics on associated type in bounds a binder

Imagine the bound for<'a> T: Trait<Item<'a>= &'a ()>. It might be that for<'a> is "too large" and it should instead be T: Trait<for<'a> Item<'a>= &'a ()>. Brought up in RFC thread here and in a few places since.

Another related question: Is for<'a> the right syntax? Maybe where<'a>? Also originally found in RFC thread here.

Stabilize lifetime GATs first

This has been brought up a few times. The idea is to only allow GATs with lifetime parameters to in initial stabilization. This was probably most useful prior to actual implementation. At this point, lifetimes, types, and consts are all implemented and work. It feels like an arbitrary split without strong reason.

History

c410-f3r, ibraheemdev, terrarier2111, BoxyUwU, messense, fmease, marmeladema, Stumblinbear, Kobzol, mental32, and 133 more reacted with thumbs up emojic410-f3r, ibraheemdev, terrarier2111, BoxyUwU, lcnr, jhpratt, messense, ickk, fmease, naim94a, and 157 more reacted with hooray emojic410-f3r, ibraheemdev, terrarier2111, xd009642, BoxyUwU, lcnr, messense, fmease, aliemjay, marmeladema, and 125 more reacted with heart emojic410-f3r, ibraheemdev, terrarier2111, BoxyUwU, darksv, messense, jakobhellermann, fmease, aliemjay, marmeladema, and 98 more reacted with rocket emojiDirbaio, SirCharlieMars, overlisted, weihanglo, Veetaha, remi-dupre, runiq, AngelOnFira, johnyenter-briars, proudmuslim-dev, and 9 more reacted with eyes emoji All reactions

rustbot

added T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-rustdoc Relevant to the rustdoc team, which will review and decide on the PR/issue.

labels

on May 4

Collaborator

rust-highfive commented on May 4

Some changes occurred in src/tools/rustfmt.

cc @rust-lang/rustfmt

calebcartwright and schneiderfelipe reacted with thumbs up emoji All reactions

rust-highfive

added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label

on May 4

Contributor

c410-f3r commented on May 4

Thank you @jackh726. Like, really, thank you very much!

jackh726, aliemjay, Kobzol, Nilstrieb, conradludgate, Dengjianping, jamesmunns, kellerkindt, bryanhitc, jdahlstrom, and 57 more reacted with heart emoji All reactions

jackh726

added T-lang Relevant to the language team, which will review and decide on the PR/issue.

and removed T-rustdoc Relevant to the rustdoc team, which will review and decide on the PR/issue.

labels

on May 4

Contributor

bors commented on May 5

umbrella The latest upstream changes (presumably #96593) made this pull request unmergeable. Please resolve the merge conflicts.

Member

nrc commented on May 5

I don't think we should stabilise GATs now (and I'm not convinced we should stabilise GATs at all). I'm sorry this is going to be a negative post, I'll try and be as positive as possible. I really appreciate the work that has gone into this feature and for help in answering my questions along the way.

I think it is very important to make a strong argument that not only can we add GATs to the language, but that we should. This is an important decision. GATs are probably the largest change to the type systems since associated types (pre 1.0), and certainly the largest change since 1.0. They're also the largest addition in complexity since way before 1.0 and the language feature with the largest possibility to change the character of the language since before 1.0.

I think that a decision on GATs is a fairly straightforward trade-off between complexity and expressivity. I'll address both sides of the trade-off.

Complexity

Rust is often criticised for being an overly complex language. From last year's annual survey, 33% of users said their biggest worry for the future of Rust was that it would become "too complex" (the second highest ranked answer). Languages which support GATs, HKTs, or similar typically have even worse reputations than Rust for complexity and learning curve.

GATs nearly always increase complexity. They introduce new syntax and new semantics to the type system. There are a few use cases for lifetime-GATs which simplify types, but mostly GATs are useful for expressing new abstractions which are inherently difficult for many programmers to understand.

GATs' complexity is not a 'zero-cost abstraction' (in the sense that you only pay the price for a feature if you use it): GATs will primarily be used by library authors and are part of the API, thus programmers will not get a choice to avoid them. They will be part of libraries and if programmers want to use those libraries, they must learn about GATs (compare to async, where if a programmer is not doing async programming, they don't need to know about async or await).

GATs' complexity is not restricted to advanced programmers. Since GATs are used in APIs, they cannot be hidden only in implementations where only advanced programmers (as library authors are likely to be) need to care about them. They are exposed to all programmers (compare to unsafe coding features, which can be completely encapsulated).

GATs are a feature which appeal to language geeks and compiler hackers and can even be fairly intuitive to us, but which are terrifying for most programmers. Note how popular Haskell is with PL/compiler people, but how it is largely shunned by industry.

Expressivity

GATs clearly increase expressivity, but I think that a solid argument that the expressivity is useful has not been made. Furthermore, the increased expressivity changes the character of Rust significantly.

There are use cases for GATs, but very few of them have been proved out. I don't know of any use cases which have been implemented and demonstrated to be significantly useful. I realise there is a reluctance to use unstable features, and this is a high bar. But the bar should be high - this is a huge change to the language.

There are numerous cases of small bugs or small gaps in expressivity which have prevented people using GATs for the use cases they want to use them for (see e.g., the blog post linked from the OP, or this reddit thread). These are the sort of things which must be addressed before stabilisation so that we can be sure that they are in fact small and not hiding insurmountable issues.

GATs have a strong use case inside the compiler as part of the implementation of async methods or impl Trait in traits. However, there is no requirement that GATs need to be exposed to the user to facilitate this usage. I think using GATs as a principled internal representation is fantastic, but that does not require exposing them to users.

Most use cases (and certainly most of the compelling use cases) are for lifetime GATs. I think that we must separately justify lifetime and type GATs, and we could add one to the language without adding the other (e.g., we have HRTBs for lifetimes but not for types). Note also that lifetime GATs are categorically simpler than type GATs because the grammar of types is structural and recursive, whereas the grammar of lifetimes is simple. Furthermore, lifetimes are part of what makes Rust unique and thus expressivity at the cost of complexity is more essential to the language in the lifetime case.

Finally, GATs increase expressivity but in a way which takes Rust in a new direction. GATs and HKTs more generally are a fine way of building abstractions, but they are not the Rust way. We use concrete types like Option and Result rather than monads, we use borrowed references to abstract over storage rather than an abstract pointer type, and we have found many times that abstractions like 'collection' or 'number' are not good fits for most libraries. There is probably a fine language which is something like Rust with abstractions built around HKTs and similar type system ideas, but that language is not Rust. It would require a different standard library, different ergonomics, and different programming idioms.

mejrs, petervaro, afetisov, oslac, mattias-p, PhilipDaniels, overlisted, Shamazo, hawkingrei, BurntSushi, and 47 more reacted with thumbs up emojisurban, marmeladema, c410-f3r, cjwcommuny, Progdrasil, TennyZhuang, CraftSpider, phaazon, fakeshadow, Dirbaio, and 60 more reacted with thumbs down emojiyasammez, andreytkachenko, marmeladema, TennyZhuang, demurgos, Globidev, johnyenter-briars, QnnOkabayashi, ivan770, gtsiam, and 11 more reacted with confused emojipetervaro, overlisted, mominul, Systemcluster, phaylon, ldanko, ChayimFriedman2, vrmiguel, dhardy, j-hc, and 10 more reacted with heart emojirubdos, darksv, slanterns, Enet4, runiq, tshepang, johnyenter-briars, Xunjin, yct21, CGMossa, and 6 more reacted with eyes emoji All reactions

Contributor

zesterer commented on May 5

edited

@nrc I agree with the sentiment of much of your post, but disagree on the specifics. Rather, I think the problem of teaching and complexity is a pervasive problem across Rust as a whole, and I don't think that holding back features is a particularly effective way to solve the problem.

As-is, I've seen many APIs in the wild that are unnecessarily complex because the author didn't have access to GATs, requiring them to create absurd abstraction towers like custom type family traits in order to achieve similar expressivity. GATs don't really enable any capability that didn't exist before, but they definitely have the power to simplify many of the more weird cases of type astronomy.

I don't know of any use cases which have been implemented and demonstrated to be significantly useful.

To this, I'd like to provide a specific and solid example of GATs (and in particular, type GATs) proving extremely useful.

I work on chumsky, a parser combinator crate. I've recently been experimenting with GATs internally as a way to control exactly what code Rust generates. Instead of praying to the LLVM gods that the compiler might optimise things, I use a GAT to project a particular parsing 'strategy' into the implementation of parsers. I've found that I can significantly improve the performance of the library by an order of magnitude, even beating out hand-written parsers, nom, and serde_json (with several caveats) without harming the library's expressivity (and, in fact, improving it). This all happens without the GATs themselves being exposed to library users at all.

I strongly suspect that many similar use-cases will appear in the future, given the possibilities GATs open up to specialise the implementation of functions, predicated upon a generic type. As far as I'm aware, no other languages that support GATs (or adjacent features like HKTs) have the same monomorphisation and performance promises that Rust has, so this space remains mostly unexplored. But, without stabilisation, this space is not open for exploration by API authors that care about performance.

marmeladema, c410-f3r, cjwcommuny, rrbutani, slanterns, therewillbecode, Progdrasil, TennyZhuang, Xuanwo, bstrie, and 68 more reacted with thumbs up emojiChayimFriedman2 reacted with confused emojijam1garner, AngelOnFira, bew, johnyenter-briars, QnnOkabayashi, praveenperera, gtsiam, CGMossa, tux3, trevyn, and 10 more reacted with heart emojiEnet4, runiq, overlisted, tshepang, Timmmm, gtsiam, Ayawen01, kotatsuyaki, and lassipulkkinen reacted with eyes emoji All reactions

Contributor

NobodyXu commented on May 5

@zesterer Can you please explain on how you use GAT to speedup your parser?

jjpe, MavethGH, SirCharlieMars, schneiderfelipe, and bryanhitc reacted with thumbs up emoji All reactions

Contributor

zesterer commented on May 5

edited

@NobodyXu

Parser combinators are parsers composed of smaller parsers, similar to how Iterator chains are created compositionally using generic types like std::iter::Map<T, F>. You might have a parser for a pattern named a, and another named b, and desire to create a new parser that parses one pattern and then the other: a.then(b).

There are a number of cases where it's necessary to parse a pattern without actually evaluating its output. For example, a.separated_by(b) (which parses a b a b a ...) produces a Vec<output of a>, but the output of b goes unused. Currently, the library evaluates the outputs of both a and b but discards the latter.

This can be very wasteful though, particularly if the creation of b's output requires allocation, such as in the case of ident.separated_by(whitespace.repeated()) (we end up with many discarded Vec<char>s).

With GATs, we can specialise the invocation of each parser's parse function with a type that controls whether an output value gets actually generated or not at compile-time (in effect, a restricted form of the monad pattern) that uses a GAT to work across whatever types the implementation of the parser cares for.

The beautiful part is that this has no impact on the user-facing API, but 'magically' speeds up the parser by statically guaranteeing that unnecessary work will be skipped, allowing something like a.repeated() to only allocate if it's used in a context where the output value is actually needed.

Although the details of this case are quite specific to chumsky, I believe this general pattern - GATs as a way to generically define an operation with types known only to the implementation - is generally useful for a lot of code, as is visible in existing languages with GATs/HKTs. Where Rust really hits the ball out of the park is that it guarantees monomorphisation, allowing these patterns to be truly zero-cost. It's difficult to overstate just how powerful that is.

c410-f3r, rrbutani, slanterns, mattias-p, tinaun, Progdrasil, TennyZhuang, NobodyXu, bstrie, zslayton, and 43 more reacted with thumbs up emojijam1garner, AngelOnFira, Globidev, bew, GoldsteinE, numToStr, raftario, gtsiam, tux3, jkugelman, and 10 more reacted with heart emojibew, ChayimFriedman2, nasso, Stumblinbear, Kobzol, schneiderfelipe, and abbudao reacted with rocket emoji All reactions

overlisted commented on May 5

@nrc I don't think that libraries exposing complex stuff in their public interface is necessarily a bad thing. It should be up to the library author to make it clear how to use it. Serde has a short explanation of HRTBs right in their docs.

Contributor

tinaun commented on May 5

wild alternative number 3: only stabilize type GATs

while I don't know how possible this is in practice, (since Item is a supertype of Item<&'a T> after all) but to me it seems like the vast majority of the footguns and undesired complexity that can make dealing with gats suprisingly unergonomic is around LendingIterator and friends, but not around "simple" generic types like in zesterer's example.

At the very least, moving the marketing of this feature away from LendingIterator and lifetime GATs in general and towards concrete ergonomic and performance wins with "simple types" in the short term could be useful in reducing complexity for the end user to understand

jplatte and schneiderfelipe reacted with thumbs up emojiKolsky, james7132, TheRawMeatball, SirCharlieMars, ColonelThirtyTwo, and optozorax reacted with thumbs down emojioverlisted, Progdrasil, ibraheemdev, Kolsky, and james7132 reacted with confused emoji All reactions

Member

BurntSushi commented on May 5

I think I might be one of the people who has wanted GATs the longest. My desire for them predates my lame attempt at working around their absence many moons ago. The first time I realized the Iterator trait was insufficient for what I wanted was before Rust 1.0 in 2014 when I wrote one of the first versions of the csv crate. All I wanted to do was write an iterator that lent out a borrow of an internal buffer in order to avoid allocating a new record on each iteration. So it's been... about eight years of waiting for me. :-) I am very appreciative of all the work @jackh726 that you've done on this! It looks absolutely amazing and it's really cool to see Rust code making use of GATs and working.

With all that said, I do unfortunately tend to agree with quite a bit of what @nrc is saying.

There are numerous cases of small bugs or small gaps in expressivity which have prevented people using GATs for the use cases they want to use them for (see e.g., the blog post linked from the OP, or this reddit thread). These are the sort of things which must be addressed before stabilisation so that we can be sure that they are in fact small and not hiding insurmountable issues.

I'm quite sympathetic to GATs exploding the complexity budget of Rust, but I think this is one of the more compelling points in terms of not stabilizing them as-is. I understand the idea of making incremental progress, but as a total outsider to lang development, this feature looks like it has way too many papercuts and limitations to stabilize right now. Not being able to write a basic filter implementation on a LendingIterator trait in safe code is a huge red flag to me. Namely, if I can't write a filter adaptor in safe straight-forward code, then... what else can't I do? Add on to that the lack of dyn traits and the inability to implement other types of adaptors like a WindowsMut, and it feels like there is just too much that can't be done. What's worse, I don't even know how to articulate in a sentence or two what GATs can or can't be used for if they're stabilized as-is.

In terms of making incremental progress, what does it look like to use GATs as an implementation detail to make async traits work? Is there a reason why we shouldn't start there?

I do appreciate that there is a chicken-and-egg problem here. It's hard to get the experience I think we need with people actually using GATs before stabilizing them. I'm not sure how to approach that problem other than taking a more conservative incremental approach here.

afetisov, Enet4, NobodyXu, jbuckmccready, ChayimFriedman2, Michael-F-Bryan, YaLTeR, jplatte, Timmmm, clintfred, and 35 more reacted with thumbs up emojic410-f3r and Globidev reacted with confused emojiSystemcluster, runiq, mominul, golddranks, CGMossa, tux3, HeroicKatora, Ayawen01, eminence, SirCharlieMars, and 2 more reacted with heart emoji All reactions

Contributor

jam1garner commented on May 5

edited

In response to @nrc (and making every section collapsible to not take up 2 vertical screen widths):

"GATs nearly always increase complexity"

  = coreresult 

  
     
  = coreresult 

  
     a

GATs' complexity is in the public API

 de  
       ->  
    
         de

"if a programmer is not doing async programming, they don't need to know about async or await""Haskell is [...] largely shunned by industry"

Now for the parts where I agree with you, because while I don't agree with your conclusion, I absolutely found every point you raised to be well-constructed and at minimum worth consideration.

"abstractions like 'collection' [...] are not good fits for most""Most use cases [...] are for lifetime GATs"

  
     a
    
       _

In the end though, I think at minimum we should really be stabilizing lifetime GATs. I personally am not too worried about type GATs, however I think this cost/benefit of holding them back is far more agreeable than lifetime GATs. So even if the route is "don't stabilize everything" out of an abundance of caution, I would still heavily implore those making the final call to at minimum shoot for lifetime GATs being stabilized this cycle.


edits

Response to BurntSushi

   

	 
	a   a a 

Also this part from @zesterer describes my needs quite nicely:

Although the details of this case are quite specific to chumsky, I believe this general pattern - GATs as a way to generically define an operation with types known only to the implementation - is generally useful for a lot of code,

This is a pattern that I find myself stubbing my toe on quite often (as someone who writes a lot of libraries focused on ease-of-use, not as a normal user). Very well put.

c410-f3r, zesterer, overlisted, NobodyXu, dgiger42, slanterns, Xuanwo, demurgos, rrbutani, Progdrasil, and 38 more reacted with thumbs up emojizesterer, overlisted, bstrie, runiq, rrbutani, tshepang, Progdrasil, wesleywiser, ChayimFriedman2, Drakulix, and 15 more reacted with heart emoji All reactions

phaazon commented on May 5

edited

I genuinely hate to be the person to point this out but... isn't async a prime example of the opposite? There's definitely been a non-zero amount of the ecosystem that falls into the category of "may be used in async or synchronous contexts".

Yeah, I wanted to say that too. A function such as:

async foo() -> i32

is not a zero-cost abstraction either, both in terms of implementation and cognitive complexity (someone who doesn’t know async programming in Rust would assume the function returns i32 while it doesn’t — it, in fact, returns impl Future<Output = i32>).

So GAT requires learning… like pretty much everything a typed language provides. Making a point on the tradeoffs here using something that is already a pretty big tradeoff (and distracting, because the types are harder to read with async) is a bit offsetting to me.


As-is, I've seen many APIs in the wild that are unnecessarily complex because the author didn't have access to GATs, requiring them to create absurd abstraction towers like custom type family traits in order to achieve similar expressivity. GATs don't really enable any capability that didn't exist before, but they definitely have the power to simplify many of the more weird cases of type astronomy.

Yeah, I struggle a lot with that in luminance. It requires creating bazillions of traits. The problem is the same with pretty much anything else that would fall into @nrc comment about complexity (HKT, rank-2 types, etc.), which have valid use cases in lots of libraries (luminance is a ecosystem I maintain so I can talk about it, but I also have issues with EDSLs for instance).

overlisted, Enet4, zesterer, NobodyXu, Progdrasil, Globidev, Pzixel, gtsiam, recatek, 8573, and 2 more reacted with thumbs up emoji All reactions

Contributor

Stargateur commented on May 5

edited

@phaazon "is not a zero-cost abstraction" zero cost abstraction doesn't mean zero cost, it's mean "you would have the same result doing it by "hand"". So you use here is wrong, async being zero cost abstraction mean that if you want do the same feature that offer async by hand you would not gain speed. (thus I don't talk about implementation here) What does 'Zero Cost Abstraction' mean?

Emerentius and kadiwa4 reacted with thumbs up emojiCGMossa and Stumblinbear reacted with thumbs down emojiphaazon reacted with confused emoji All reactions

Member

BurntSushi commented on May 5

edited

@jam1garner

Also I would definitely say I agree with @BurntSushi that the most compelling reason for me as to why we might hold back GATs isn't complexity or anything

I am also quite sympathetic to the complexity argument here too. And I think I see the complexity as very different than you, based on what you've written. That GATs make the language more consistent in some corners does not really hold much sway with me in terms of "complexity" here. What I think of, and what I think @nrc is thinking, is the emergent complexity of abstractions that are enabled by GATs. I don't really know how to untie the knot here in terms of teasing these different sorts of complexities apart, but they are very different ideas that unfortunately can both be reasonably described as "complexity." If someone wanted to start a Zulip stream on this topic, I'd be happy to try and explain this viewpoint a bit more because I think it's a huge topic that has the potential to derail this thread.

Are there papercuts which allowing upper bound constraints on HRTB lifetimes (relevant snippet from Sabrina's blog post below) don't cover?

I have absolutely no idea. I have no tools for how to even think about an answer to this question. :-/

My impression is that improvements would be backwards compatible by allowing trait bounds that currently aren't expressible to be opted into (allowing for constraining lifetimes in such a manner that trait bounds can be expressed in the places that currently cause issues). To me, this feels more like const generics: sure, a lot of things can't be expressed yet, they just need more work. So in a sense, I guess it's more of a matter of having confidence in that being the case?

OK so thank you for mentioning this, because this is not what I had in mind. If everyone involved in Rust's type system really believes that all outstanding issues are not only resolvable, but are resolvable in a backwards compatible way, then I'm happy to trust that. They're the experts, not me. I might raise an eyebrow in surprise of such confidence given how many outstanding issues there are today personally, but no, ultimately I'd trust them.

The issue I have with GAT's incompleteness is actually about the user experience. The const analogy is a good one, because it demonstrates just how different things are here. In my understanding, it is very easy to articulate and apply the limitations of what is allowed in a const context. Just about anyone can understand things like:

  • You can't allocate in a const context.
  • You can't write loops in a const context. (No longer a restriction.)
  • You can't call non-const functions in a const context.
  • etc...

Even better than that, the limitations are so clear and crisp, that the compiler can recognize it and tell you exactly what's wrong. That's a reasonable failure mode. "They just haven't gotten to it today so I can't do that." OK. Great. Now I can move on to some other solution to my problem or whatever.

Compare that with trying to write a Filter adaptor for a LendingIterator trait. What error do you get? Does it tell you that "GATs don't currently support blah blah, so you can't express this pattern yet." No, it doesn't. Of course, maybe this is just an artifact of not having better error messages yet. But is it? From the corresponding issue, it looks like the actual problem is a limitation in the borrow checker that GATs now make easier to stumble across... So it's not necessarily easily expressible as a function of what GATs let you express, but has something more to do with how different language features intersect.

This is a really important point. Because if I try to write that Filter adaptor and get an error like that, what do you think is going to happen? Do you think I'm going to immediately give up and understand that GATs just aren't complete yet? Heck no I'm not. I'm going to keep trying to re-arrange the code because I'm pretty sure what I'm trying to do is legal. So maybe there's a contortion I can make to the code to make it work. All in all, it's an extremely frustrating experience. Now in this case, I might get lucky and search and find the Filter issue and discover that what I'm trying to do isn't possible yet. At which point, I can give up or use the work-around. But are all such limitations with GATs so easily discoverable? I don't know the answer to that question, but nothing I've seen written by anyone leads me to believe that anyone knows the answer to that question, even the experts. But maybe I'm wrong.

My advice to folks is to try and look at this feature and the experience it gives to users through beginner eyes. Heck, I'm no Rust beginner (nor even someone completely ignorant of type theory) and a lot of the failure modes I've seen with the GAT feature are really inscrutable. I think we should do one of three things:

  • Build a smoother onboarding experience (better error messages and understandability). Something akin to the grokability of what is or isn't allowed in a const context today would be great and would really assuage a lot of my concerns here.
  • Drastically decrease the number of failure modes. It's one thing to try and do something sophisticated with the type system and run into an inscrutable error message that doesn't help much. But it's another to do something reasonably basic that potentially many folks will run into and provide an unclear error message. So if you can smooth out the feature such that most interactions with it work like you'd expect, then you lessen the pressure to improve on failure modes.
  • Sidestep the above two problems by starting with a more conservative stabilization. (I am unclear on the parameters of what's possible or feasible here.)
ChayimFriedman2, Enet4, sthiele, YaLTeR, kellerkindt, jplatte, PoignardAzur, eldruin, vi, raftario, and 26 more reacted with thumbs up emojic410-f3r reacted with confused emojijam1garner, Keats, afetisov, runiq, inquisitivecrystal, mominul, faptc, andylizi, jedel1043, bew, and 15 more reacted with heart emoji All reactions

Contributor

Stargateur commented on May 5

edited

This is a really important point. Because if I try to write that Filter adaptor and get an error like that, what do you think is going to happen? Do you think I'm going to immediately give up and understand that GATs just aren't complete yet? Heck no I'm not. I'm going to keep trying to re-arrange the code because I'm pretty sure what I'm trying to do is legal. So maybe there's a contortion I can make to the code to make it work. All in all, it's an extremely frustrating experience. Now in this case, I might get lucky and search and find the Filter issue and discover that what I'm trying to do isn't possible yet. At which point, I can give up or use the work-around. But are all such limitations with GATs so easily discoverable? I don't know the answer to that question, but nothing I've seen written by anyone leads me to believe that anyone knows the answer to that question, even the experts. But maybe I'm wrong.

@BurntSushi The problem of lifetime borrow that the filter example show is not unique to GaTs, and I saw a lot of question on stackoverflow about this problem. Always solve by polonius borrow checker. I don't think this is a good argument on this case, it apply to every Rust code. But you are right everytime the user is very confuse why the code don't compile. Returning a reference from a HashMap or Vec causes a borrow to last beyond the scope it's in? for example (there are several linked question). Notice the question is from 2016

c410-f3r and CGMossa reacted with thumbs up emojic410-f3r and CGMossa reacted with rocket emoji All reactions

Member

BurntSushi commented on May 5

@Stargateur That's beside the point for a few reasons, and I don't think is really addressing the substance of my concerns.

Firstly I acknowledged that the Filter adaptor example was about the interaction of language features, so I know it's a pre-existing issue. Namely, if GATs make that borrow checker limitation more common than it already is, that isn't good. My argument is not one of mere existence. It's about the overall user experience. The work-around for this particular Filter adaptor example is also not nearly as nice as the work-arounds for the SO questions you've linked.

Secondly, the Filter adaptor code is an example, not an argument. It exemplifies how difficult it is to know when you've hit a limitation of a new language feature, or a limitation of the interaction of language features or whatever and whether you should keep trying to change your code to get the compiler to accept it. How many hours went into that blog post trying to figure out not just how to write a WindowsMut adaptor, but whether it was even possible in the first place? How many other permutations of this problem exist? I have no clue.

afetisov, jbuckmccready, faptc, jedel1043, ChayimFriedman2, YaLTeR, PoignardAzur, raftario, burjui, tux3, and 7 more reacted with thumbs up emoji All reactions

Contributor

jam1garner commented on May 5

@BurntSushi very fair points! And apologies on the phrasing, didn't draw enough of a line between where I was riffing off you and where I was stating my own opinion.

While I'm sure this isn't the greatest solution, I wonder if something like "GATs without trait bounds" is a possible path forward? (Or maybe slightly broader—no constraining lifetime GATs with trait bounds?)

I know for many that's functionally useless, it just happens to cover roughly a third of my usecases while (to my knowledge) having a good biglt fewer edge cases for {borrow, type, trait bound} checking to go wrong. Not sure how representative my usecase is though so maybe for everyone else that'd be more frustrating than anything :)

And you're right I definitely was (somewhat intentionally) construing different definitions (feature cognitive overhead vs, as you said, emergent complexity of the abstractions it enables). I'd like to think I somewhat covered how I feel about the kind you/nrc are talking about (and how I think in practice I'm not sure beginners would actually need to think about it, similar to iterator adapter trait bounds, assuming "collection traits" and the likes don't become idiomatic), however that is of course a bit handwavey as my argument isn't all that concrete.

And yeah the UX could be better :( I can't speak for everyone but as someone who tries to make diagnostics PRs when I hit issues and have the time/energy, I personally can't really kick the tires if I can't use the feature in my libraries, and thus don't have a very natural path to find pain points to. I've converted a library of mine twice (once to real GATs, once to lifetime GATs emulation via HRTBs) but it's a lot of refactoring effort (big crate, w/ derive macro, etc) only to not get too far out of trivial usage territory.

Part of me is tempted to say "if diagnostics are the blocker, stabilize and improvements will roll in", as I believe someone else said upthread it's sorta a chicken-and-egg issue. Since it's mainly a feature aimed at sufficiently complex libraries I don't think my situation (prohibitively high cost to give any real-world testing, only for branch staling to make that effort difficult to keep useful) is that uncommon.

Regardless, I'm very grateful for the work being done here. The boundaries are fuzzy and the interactions with lifetimes are pretty novel. Even if GATs get held back indefinitely I'll be happy, whatever decision is made will probably be the one I agree with in 2 year's time anyways :P

BurntSushi, bstrie, rrbutani, Progdrasil, CGMossa, gz, JoJoJet, SirCharlieMars, and abbudao reacted with heart emoji All reactions

Contributor

slanterns commented on May 5

edited
it's far from fair to try and imply a single aspect of Haskell (the degree of abstraction) is the sole cause...

I agree with that. We cannot simply attribute it to HKT, and HKT is rather intuitive (especially in Haskell) for me. The imagination of HKT in the blog post looks even attractive since it seems actually a simplification for me. Anyway, thanks everyone for pushing Rust into a better language!

8573, gtsiam, JoJoJet, and SirCharlieMars reacted with thumbs up emoji All reactions

Contributor

CraftSpider commented on May 5

I agree with the statement that HKTs are not, on their own, fundamentally unintuitive. Also, as an outside observer: why are these points being raised now, and not before many posts (including official Rust blog posts) about GATs approaching stabilization? I've been expecting them as a user as a non-controversial addition for a while now. This isn't meant as a criticism of the points, just curiosity about what happened that they only came up after the work was done.

overlisted, marmeladema, jedel1043, tshepang, Globidev, demurgos, malthesr, zesterer, Progdrasil, dgiger42, and 19 more reacted with thumbs up emoji All reactions

Contributor

Stargateur commented on May 6

edited

@CraftSpider Theoretically, we already decide to include GaTs like describe in RFC 1598, thus it's allowed to make small change or even cancel everything before it's land to stable. "why are these points being raised now" there two factors here, first it's very hard to predict how thing as complex as GaTs will go. Unexpected things happen. Secondly, the stabilisation request is the "last time" where people can raise concern. Specially the concern here is precisely about the stabilisation could be premature. It's not surprising that a feature like GaTs that could change everything in Rust attract concern like this.

Then these concerns can be look by the team associate with the stabilisation, here I think it's the Lang team that will have the last word about this.

overlisted, bew, and SirCharlieMars reacted with eyes emoji All reactions

Member

nrc commented on May 6

I'll address some technical points later, but for now...

Also, as an outside observer: why are these points being raised now, and not before many posts (including official Rust blog posts) about GATs approaching stabilization? I've been expecting them as a user as a non-controversial addition for a while now. This isn't meant as a criticism of the points, just curiosity about what happened that they only came up after the work was done.

This is partly a process failure, but also this is exceptional work both in scope, and in time between RFC and implementation (it's been 6 years since the RFC was proposed and 5 years since it was accepted). In our process for creating new features, there is no formal place for registering objections between RFC discussion and stabilisation discussion, that is usually OK, but here the time period was exceptionally long. Personally, I have registered concerns about this feature privately and publicly, but like I say there is nowhere to do that officially. The project has changed a lot since the RFC was proposed, both technically (the type system has got more complex in other ways) and non-technically (the language is much more mature now, we have many more users, and are attracting new users at a much higher rate), also people change - in six years people leave and new people arrive, and people's opinions change.

Also, it is expected that during implementation and once an implementation is available for use, we gain experience and that informs our decision making and can change people's opinions. Accepting an RFC is never a guarantee that a feature will be stabilized, and in fact it is quite rare that a feature is stabilised in exactly the same form as described in an RFC.

Enet4, zesterer, NobodyXu, Globidev, tshepang, ChayimFriedman2, raftario, tux3, Virgiel, EdorianDark, and 4 more reacted with thumbs up emoji All reactions

Contributor

kellerkindt commented on May 6

edited

As an outside observer: Having so many points raised against the stabilization while trying to stabilize this is a bit concerning. The main take-away I get from scrolling through this thread is that GATs are not ready yet (missing/limiting parts) or that it's actually unknown whether GATs in this form is production ready / the papercuts and limitations will have bad/limiting consequences in the future (missing experience).

I totally get the chicken-and-egg problem here.

Extending @BurntSushi idea to "Sidestep the above two problems by starting with a more conservative stabilization": I remember the async stabilization being driven and motivated very much by (positive) experience from nightly production usage (fuchsia team). Maybe something like that is needed for GATs as well to assess that the current approach is ready. Don't the generators for async fns in (static) traits need this feature? Maybe implementing those and therefore using GATs but (for now) only internally in the compiler will provide enough experience to confidently stabilize or adjust this feature? Then again, as an "outside observer", I don't know if this is the case already.

As a closing note, I am really baffled by the commitment of all involved. For me personally GATs are first of all the "things needed" to get async fns in traits and I am regularly surprised of how much work this actually requires.

Hats off to you folks!

NobodyXu, overlisted, ChayimFriedman2, willcrozi, JoJoJet, SirCharlieMars, bryanhitc, and kraktus reacted with thumbs up emojiGlobidev, tmandry, and SirCharlieMars reacted with heart emoji All reactions

Contributor

PoignardAzur commented on May 6

Also, as an outside observer: why are these points being raised now, and not before many posts (including official Rust blog posts) about GATs approaching stabilization?

My personal take: at first I thought positively of GATs, and my reaction was essentially "I'm looking forwards to the day this is available with stable Rust".

Then as we got closer to the deadline, more and more blog posts came out with examples of GAT code and at that point my reaction became "Wait, is that what GATs look like in practice? I don't understand any of this code. Am I going to have to read code like this everywhere soon?", hence why I'd now agree with BurntSushi that the feature still needs some baking.

For instance, reading code like this is making me very nervous:

pub trait LendingIteratorLifetime<'this>
where
	Self: 'this,
{
	type Item;
}

pub trait LendingIterator: for<'this> LendingIteratorLifetime<'this> {
	fn next(&mut self) -> Option<<Self as LendingIteratorLifetime<'_>>::Item>;
}

Member

joshtriplett commented on Aug 8

I read through the stabilization report and the documentation in the GATs repository, and this looks good to me.

@rfcbot reviewed

overlisted, praveenperera, Keelar, LouisGariepy, treysidechain, and Rageking8 reacted with thumbs up emojic410-f3r, weihanglo, daleione, dnrusakov, pooyamb, nathanielsimard, tuguzT, gz, treysidechain, dzvon, and 10 more reacted with hooray emojiikenox, Xiretza, Skgland, 0xdeafbeef, weihanglo, tiann, robinhundt, dnrusakov, tuguzT, Folyd, and 11 more reacted with heart emojiljedrz, anlihust, alice-i-cecile, bryanhitc, ikenox, 71, rami3l, denzp, Progdrasil, weihanglo, and 19 more reacted with rocket emojiEnet4, tuguzT, alice-i-cecile, ikenox, jtran, 0xdeafbeef, ollpu, nathanielsimard, treysidechain, dzvon, and 6 more reacted with eyes emoji All reactions

Member

nrc commented 29 days ago

I read through the stabilization report and the documentation in the GATs repository, and this looks good to me.

@joshtriplett there are a bunch of reasonable objections in this thread, do you have any kind of response to those? Could you explain why "this looks good to me"? You're co-lead of the language team and a prominent member of the libs team, this is arguably the largest change to Rust's type system since 1.0. I'd really like to understand a bit more of your thought process here.

CryZe, ilslv, TimNN, daleione, tuguzT, Awpteamoose, dnut, runiq, Kobzol, EAimTY, and 5 more reacted with thumbs up emojidzvon, 0xdeafbeef, Christopher-S-25, nyuichi, c410-f3r, Progdrasil, ricardoalcantara, gschulze, dojiong, bohrasd, and 14 more reacted with thumbs down emojialdanor, tuguzT, steveklabnik, and frewsxcv reacted with eyes emoji All reactions

Member

joshtriplett commented 29 days ago

edited

@nrc I've followed some of this thread, as well as some summaries of the objections, and similar discussions on Zulip. Leaving aside @nikomatsakis's summary post (which as stated looks reasonable to me), my thought process is, roughly:

  • Yes, it's possible to do complex things with this. It's also possible to do complex things with existing types (e.g. type-level computation), and for the most part people don't except as stunts. I don't have any fears of Rust turning into Haskell as a result of this change.
    • I also found it compelling that some projects (such as Bevy) have said this would improve usability for their users.
  • Regarding completeness, I'm comfortable with the stated descriptions of what we are and aren't locking in with this stabilization.
  • I value many of the uses of this, and find the examples of usage both compelling and not obviously supportable in other ways.

I absolutely do think this is a feature with the potential for a high complexity surface area. In signing off on this, I'm not ignoring or discounting the complexity; rather, I'm evaluating the trade-off between that complexity and the value it provides.

dzvon, finpluto, ljedrz, treysidechain, nitsky, tuguzT, kvxmmu, Lokathor, xd009642, cberner, and 37 more reacted with thumbs up emojipraveenperera, volllly, Christopher-S-25, dzvon, Rexagon, Drakulix, treysidechain, nitsky, tuguzT, ricardoalcantara, and 34 more reacted with heart emojiEnet4, tuguzT, and jtran reacted with eyes emoji All reactions

Contributor

Author

jackh726 commented 29 days ago

Hi all,

I haven't really posted much here. As I said before, I've wanted to try to stay out of the discussion and let the discussion continue without my influence. However, given that @nikomatsakis has proposed FCP on this, I figured I'd put on my types team hat and give my reasonings for why my box is checked. I'd also like to go ahead and give my more general opinions on this feature and its stabilization.

First, I'd like to say that I just put up a PR that slightly changes the rules for the self-outlives error (#100382). Particularly, GATs in the inputs to trait methods now factor into the lint. I'll update the initiative repo sometime soon to reflect this. (This is relevant to one of the questions for the types team.) Second, I'd like to also note that I'm planning to try to improve errors you might get when using higher-ranked trait bounds with GATs today. These will eventually be allowed, but that will come later. (I will discuss this below.)

Answering questions for T-types

Are we confident we can address the known shortcomings without backwards incompatible changes?

I am confident, yes. To me, there are a few shortcomings to the current implementation and design:

  1. Higher-ranked trait bounds (e.g. for<'a> Self::Item<'a>: Send) leads to : 'static requires. We've discussed this quite a bit in the GATs initiative meetings and also in the types team meetings a few times. The obvious fix is to make this essentially for<'a where WF(Self::Item<'a>)> .... Niko and others are making good work to prototype this in a-mir-formality and this change is not backwards-incompatible. In the meantime, I'm working on improving errors to better acknowledge/hint that this is a GAT problem.

  2. The "spooky action at a distance of trait bounds". I've tried to remove this requirement (https://github.com/jackh726/rust/tree/gats-no-trait-where), e.g. make the WF-ness of GATs implied and not require the trait bounds. Unfortunately, it doesn't work and is unsound. There might be a way to fix this, but it probably requires tweaking some rules that aren't GATs and are already stable. That might, in theory, be able to be done over an edition. In that case, the actual GAT syntax wouldn't really change, just become more lenient. The other side of this coin would be to imply these bounds on the trait by default. This is allowed because we currently require these bounds, except when the GAT appears in where clauses (well, with my above-mentioned PR). Omitting this analysis from where-clauses makes sense, to me, since they don't participate in other implied-bounds.

  3. The ugly (and maybe confusing) for<'a> T: Trait<Assoc<'a> = U> trait bound syntax. There are a few alternatives here (T: Trait<for<'a> Assoc<'a> = U>, T: Trait<Assoc<'_> = U>). Unfortunately, these don't really solve the case where you have something like fn foo<'a, T: Trait>() where T: Trait<Assoc<'a> = U>. which generally isn't what you want. We're at the mercy of the existing Rust syntax, sadly. We might want to warn when we detect potentially incorrect cases, but I don't think we want to forbid it.

  4. The limitations with the current borrow checker and closure-based APIs. Namely things like the filter Iterator adapter (Filter adapter for LendingIterator requires Polonius #92985). This is unfortunate, but will be fixed eventually, when the next-generation borrrow checker (Polonius?) comes.

  5. The difference between associated type bounds and where clauses. This, I think, has had the least experimentation done with it, but has led to some confusion in the past. Nobody has really brought up what an alternative here would look like, and these rules have been in place for a couple years now. If we did want to change these, they migth be able to be done in a backwards-compatibile manner, or we might be able to do it over an edition.

Are we confident that we have the right rules for required bounds?

With my above-mentioned PR, yes. I think GATs in the arguments are a fine extension to the current rules. As I also mention above, I think not extending to where clauses also makes sense given the lack of implied-bounds for types appearing in where clauses.

General thoughts on stabilization

First off, I'm biased here. But, I'm biased in the best way possible: I've been working on this feature for over a year now. I know the bugs; I know the limitations. But I also know how much excitment and readiness for the feature there is. And I know that people use GATs on nightly, despite the limitations listed above.

If the problem for stabilization is that the feature is too complex for Rust, then I think that's maybe an incomplete argument. It is indeed a complex feature, but with that complexity comes power. And I have yet to see a comprehensive response for an alternative to GATs to solve the problems that GATs solve. It's very disheartening to see this argument continue to be made without a concrete counter solution in place.

If the problem for stabilization is the design of certain parts of the feature: I get this. As I list above, I know they exist. However, the design today is in a place where we follow existing Rust-isms most closely (No new syntax position for the for<'a>, no elision for the for<'a>, no implied bounds). The design might evolve over time to make the syntax more clean, but it's not like we're introducing new syntax, really).

If the problem for stabilization is the bugs: I also get this. But I think the feature is worth stabilizing despite these. The biggest bug, I think, is the HRTB 'static problem - but there is a path forward here. Nightly crates have been able to use GATs despite this and other bugs. The feature is usable. It doesn't crash the compiler when you use the feature. There isn't going to be some fundamental shift in the implementation after stabilization. There may be bugs, yes, but they don't undermine the use of the feature.

Finally, if the concern is that, if we stabilize, we're going to forget about GATs and leave them in the current state, I think that's maybe a little misguided. Sure, there are bugs (several unsound bugs, to add) that have been sitting in the issue tracker for years, so some might be wary of adding more surface area to the Rust langugage - and implementation. However, this area, the type and trait system, is precisely what the types team was formed to oversee and work on. Some of the limitations above - HRTB implied bounds, the borrow checker - are questions the types team is actively working on as part of efforts to "formalize" the type checker and trait solving. The GATs initiative isn't going away either - we will continue to fix bugs and consider design improvements (such as the trait bound syntax). I personally feel confident that progress will continue to move forward.

Final thoughts

All this being said: Let's stabilize GATs. Yes, there are limitations currently. But they solve many problems Rust users have - despite those limitations. And, there are multiple people that are working together to remove the limitations that do exist now. Letting GATs sit unstable doesn't help anyone. People that are on nightly because of GATs must stay on nightly. People who can't use GATs because they aren't stable still can't use them. The feature isn't going to change in backwards-incompatible ways. And the feature doesn't move any faster than if it were stable.

afetisov reacted with confused emojic410-f3r, TimNN, ryzhyk, runiq, Kobzol, audunhalland, MabezDev, Rexagon, marmeladema, Globidev, and 84 more reacted with heart emojiEnet4 reacted with eyes emoji All reactions

Member

nrc commented 28 days ago

@nrc I've followed some of this thread, as well as some summaries of the objections, and similar discussions on Zulip. Leaving aside @nikomatsakis's summary post (which as stated looks reasonable to me), my thought process is, roughly:

@joshtriplett thanks for expanding!

Enet4 and alenpaul2001 reacted with eyes emoji All reactions

Member

nrc commented 28 days ago

Are we confident we can address the known shortcomings without backwards incompatible changes?

I'm glad there has been some progress here. Points 2, 3, and 5 still sound like there are significant unknowns though and I'd much rather these were addressed before stabilisation.

And I have yet to see a comprehensive response for an alternative to GATs to solve the problems that GATs solve. It's very disheartening to see this argument continue to be made without a concrete counter solution in place.

I think this is the wrong way to approach this question. It is assuming that we must address the expressivity question in some way and that we need to decide how to address it. I think the question ought to be whether we need to address the expressivity question at all. In any language there are going to be use cases which are hard/ugly/impossible to express. The only 'solution' to that is to grow the language until it is infinitely large, which has obvious downsides. We have to draw a line somewhere, maybe that line includes GATs (obviously I don't personally think so, but it does seem a reasonable opinion that it should) but I'd love for us to be talking explicitly about where we draw the line and why, rather than pretending that there is no line (or no need for a line).

If the problem for stabilization is the design of certain parts of the feature ...

I don't think there is an issue with having some well-defined work still to do (we often stabilise features with known further work items) or that there are some minor bugs (it's software!), but rather that there are more issues, and that these issues seem larger and to be more unknown than we usually expect when stabilising a feature. That makes it feel like stabilisation is risky - we could be trapping ourselves out of a better solution.

It might be that you are more certain about things than the write up suggests. I get the impression you are trying to be exhaustive and unbiased in your descriptions of the remaining issues. If you think that there what we currently have is the best we could have and the alternatives are just worse, rather than needing more work to decide if they are worse or better, then that would actually make me feel better about the stabilisation, because it means there is a much smaller space for exploration going forward.

BurntSushi, EAimTY, gtsiam, 2788, afetisov, steveklabnik, EdorianDark, and MortenLohne reacted with thumbs up emojidzvon, Christopher-S-25, Progdrasil, vultix, cberner, treysidechain, BeyondToBe, dnrusakov, and Carter0 reacted with confused emojiEnet4 reacted with eyes emoji All reactions

Contributor

c410-f3r commented 28 days ago

Kindly asking as an user that wants to use a feature: Is it possible for the remaining team members to cast their votes? With 160 comments and 3 months of discussions, I guess there are already enough arguments from both sides to at least reach a decision based on majority.

alice-i-cecile, Rexagon, davidpdrsn, overlisted, dojiong, tuguzT, zesterer, gschulze, rami3l, olanod, and 10 more reacted with thumbs up emojicompiler-errors reacted with thumbs down emojislanterns and robinhundt reacted with confused emoji All reactions

Contributor

eggyal commented 28 days ago

Kindly asking as an user that wants to use a feature: Is it possible for the remaining team members to cast their votes? With 160 comments and 3 months of discussions, I guess there are already enough arguments from both sides to at least reach a decision based on majority.

Perhaps I'm speaking out of turn, but just because you aren't seeing activity on this issue doesn't mean that it isn't being actively discussed amongst the team. On Zulip, for example, there have been active conversations including with those who have not yet cast a vote.

This is a big change to the language (perhaps the biggest since 1.0), and once stabilised there's really no going back. I think it's right and proper that people are given the time and space to really think through all of its ramifications before they reach a decision.

That said, I am also very keen to see this stabilised. :)

robinhundt, jjpe, tiberiusferreira, rami3l, davidpdrsn, kamulos, NobodyXu, slanterns, praveenperera, jtran, and 23 more reacted with heart emoji All reactions

Contributor

slanterns commented 28 days ago

Kindly asking as an user that wants to use a feature: Is it possible for the remaining team members to cast their votes? With 160 comments and 3 months of discussions, I guess there are already enough arguments from both sides to at least reach a decision based on majority.

Perhaps I'm speaking out of turn, but just because you aren't seeing activity on this issue doesn't mean that it isn't being actively discussed amongst the team. On Zulip, for example, there have been active conversations including with those who have not yet cast a vote.

This is a big change to the language (perhaps the biggest since 1.0), and once stabilised there's really no going back. I think it's right and proper that people are given the time and space to really think through all of its ramifications before they reach a decision.

That said, I am also very keen to see this stabilised. :)

I agree, better to leave it to the lang team's independent decision process than put it in urgency.

Contributor

Author

jackh726 commented 28 days ago

I'm glad there has been some progress here. Points 2, 3, and 5 still sound like there are significant unknowns though and I'd much rather these were addressed before stabilisation.

No, there are not significant unknowns. These are the limitations and where the design is. I'm not sure what you expect to address. For the trait bounds, these are necessary for soundness. For the trait bound syntax, these are the status quo of stable Rust syntax. For the difference between associated type bounds and where clauses, I have yet to see an alternative proposal here.

Any changes to 2 and 3 would be extensions to the current feature. And change for 5 could be done in a backwards-compatible manner or over an edition - but again, have yet to see alternatives proposed.

I think this is the wrong way to approach this question. It is assuming that we must address the expressivity question in some way and that we need to decide how to address it. I think the question ought to be whether we need to address the expressivity question at all. In any language there are going to be use cases which are hard/ugly/impossible to express. The only 'solution' to that is to grow the language until it is infinitely large, which has obvious downsides. We have to draw a line somewhere, maybe that line includes GATs (obviously I don't personally think so, but it does seem a reasonable opinion that it should) but I'd love for us to be talking explicitly about where we draw the line and why, rather than pretending that there is no line (or no need for a line).

I hate to seem a bit rude here, but this question has been discussed. It was discussed on the RFC; it's been discussed over the years since the RFC during implementation; it's been discussed in this thread. The lang team checking their boxes on the proposed FCP affirm that this feature fits into the language. If you want to argue against it, then you should give concrete examples where this feature does not fit into Rust. I'm tired of general "this is too complicated for Rust" arguments that lead us in circles.

I don't think there is an issue with having some well-defined work still to do (we often stabilise features with known further work items) or that there are some minor bugs (it's software!), but rather that there are more issues, and that these issues seem larger and to be more unknown than we usually expect when stabilising a feature. That makes it feel like stabilisation is risky - we could be trapping ourselves out of a better solution.

Whether the limitations I listed are "large" is a bit subjective. But they are not unknown - we know exactly what they are and what potential future extensions might look like. Importantly, those extensions are backwards-compatible with the current state.

It might be that you are more certain about things than the write up suggests. I get the impression you are trying to be exhaustive and unbiased in your descriptions of the remaining issues. If you think that there what we currently have is the best we could have and the alternatives are just worse, rather than needing more work to decide if they are worse or better, then that would actually make me feel better about the stabilisation, because it means there is a much smaller space for exploration going forward.

Okay, then let me be crystal clear: I am overwhelmingly certain that stabilizing GATs is the correct next action. As I said, in my post above, the limitations I describe above can be removed in a backwards-compatible manner and do not undermine the feature itself. There are no proposed or theoretical alternatives to this feature, only extensions.

zesterer, cberner, treysidechain, dzvon, geom3trik, printfn, finpluto, dojiong, BeyondToBe, dnrusakov, and 14 more reacted with thumbs up emojiLouisGariepy, MabezDev, gtsiam, cberner, treysidechain, dzvon, geom3trik, dojiong, BeyondToBe, JonathanxD, and 6 more reacted with hooray emojiProgdrasil, praveenperera, nappa85, lu-zero, AloeareV, 71, alice-i-cecile, ricardoalcantara, alexforster, bryanhitc, and 45 more reacted with heart emojibryanhitc, MabezDev, darksv, cberner, treysidechain, dzvon, geom3trik, dojiong, BeyondToBe, JonathanxD, and 7 more reacted with rocket emojiEnet4 and ohsayan reacted with eyes emoji All reactions

Member

nrc commented 27 days ago

No, there are not significant unknowns. These are the limitations and where the design is.

There are no proposed or theoretical alternatives to this feature, only extensions.

OK, thanks, that is clear and I misunderstood your earlier description (there were a lot of "might"s and "alternative"s there, but I think you were just trying to be complete).

but this question has been discussed

I think we must agree to disagree here. In any case, this is something I expect from the lang team rather than you; you've done an excellent job of communicating the status of the project and the strengths of the proposal.

Whether the limitations I listed are "large" is a bit subjective. But they are not unknown - we know exactly what they are and what potential future extensions might look like. Importantly, those extensions are backwards-compatible with the current state.

Thanks for clarifying!

tuguzT, ilslv, slanterns, jackh726, TimNN, N9199, stijnfrishert, praveenperera, darksv, LouisGariepy, and 7 more reacted with thumbs up emojiEnet4 reacted with eyes emoji All reactions

PureWhiteWu commented 16 days ago

edited

Hi, all.

As a real production use case for GAT(and TAIT), we have open-sourced our internal RPC framework Volo: https://github.com/cloudwego/volo and its middleware abstraction layer Motore(which is greatly inspired by Tower): https://github.com/cloudwego/motore. Here's a feedback for that.

We have also met some problems when GAT is used together with some other features, and we have submitted an issue for that #100267 (which caused our docs fail to build). And besides that, we have also encountered #92096 and #64552.

Though there may still be some bugs with GAT, GAT (together with TAIT) are super useful features and we are quite looking forward for their stabilizations.

Thank you for all your great works!

bryanhitc, alice-i-cecile, jjpe, tuguzT, robinhundt, zesterer, gtsiam, davidatsurge, eldruin, pooyamb, and 10 more reacted with thumbs up emojituguzT, compiler-errors, zesterer, gtsiam, Globidev, treysidechain, tvallotton, N9199, dureuill, faern, and 2 more reacted with heart emoji All reactions

Contributor

ohsayan commented 13 days ago

Hey everyone,

I am going to chime in here regarding the stabilization of GATs. Our use-case at Skytable is very much similar to the example highlighted in RFC 1598, where we want to implement a single trait for a common group of data structures. GATs will enable us to do "hot swapping" of the underlying DS to test for performance impacts.

Our goal is to have something like below (please note this is just an example):

#![feature(generic_associated_types)]
use std::collections::LinkedList;
trait MemoryStore {
    type Item<'a>  where Self: 'a;
    type Iterator<'a>: Iterator<Item = Self::Item<'a>>  where Self: 'a;
    fn get_iter<'a>(&'a self) -> Self::Iterator<'a>;
}

impl MemoryStore for Vec<u8> {
    type Item<'a> = &'a u8;
    type Iterator<'a> = std::slice::Iter<'a, u8>;
    fn get_iter<'a>(&'a self) -> Self::Iterator<'a> {
        self.iter()
    }
}

impl MemoryStore for LinkedList<u8> {
    type Item<'a> = &'a u8;
    type Iterator<'a> = std::collections::linked_list::Iter<'a, u8>;
    fn get_iter<'a>(&'a self) -> Self::Iterator<'a> {
        self.iter()
    }
}

pub struct Store<M: MemoryStore> {
    data: M,
    ..
}

To test new structures, we'd simply implement MemoryStore and then set the type to Store<MyStore>. The Store structure would provide high level methods. To sum up, GATs would make testing much much simpler for us than it is today.

tuguzT, praveenperera, glebpom, TimNN, dnrusakov, bryanhitc, mijamo, SergeyKasmy, and kraktus reacted with thumbs up emoji All reactions

Contributor

est31 commented 12 days ago

wave Hello, I'm writing this comment in this stabilization PR to notify you, the authors of this PR, that #100591 has been merged, which implemented a change in how features are stabilized.

Your PR has been filed before the change, so will likely require modifications in order to comply with the new rules. I recommend you to:

  1. rebase the PR onto latest master, so that uses of the placeholder are possible.
  2. replace the version numbers in the PR with the placeholder CURRENT_RUSTC_VERSION. For language changes, this means the version numbers in accepted.rs (example: 4caedba). For library changes, this means the since fields (example e576a9b).

That's it! The CURRENT_RUSTC_VERSION placeholder will, as part of the release process, be replaced with the version number that the PR merged for. It can be used anywhere in rust-lang/rust, not just accepted.rs and the since fields.

If you have any questions, feel free to drop by the zulip stream, or ping me directly in this PR's thread. Thanks! wave

overlisted and nikomatsakis reacted with thumbs up emojijackh726 reacted with heart emojiEnet4 reacted with eyes emoji All reactions

Collaborator

rustbot commented 9 days ago

Some changes occurred in src/tools/rustfmt

cc @rust-lang/rustfmt

compiler-errors, Nilstrieb, and Uriopass reacted with laugh emoji All reactions

Contributor

rbtcollins commented 8 days ago

Another data point, good or bad. I write some crates that do API client things, but users of the crate don't want to have to choose between async-std and tokio - so GATs would help in allowing a trait abstracting over those ecosystems, or any new one that arises. Today we've avoided anything particularly troublesome, though abstracting over async spawn required a trait method for each output type needed, because of JoinHandle's polymorphism.

bliednov, bryanhitc, NobodyXu, oscartbeaumont, and tema3210 reacted with thumbs up emoji All reactions

Member

pnkfelix commented 8 days ago

edited

Based on my review of the comments in this thread, and also based on the T-lang design meeting discussion today, I am inclined to move forward with the plan to stabilize GATs (both lifetime and type) as they stand today.

There are some subpar developer experiences in the implementation as it stands today. Some of those are bugs that we should able to address, and others will require longer term work on either the compiler's static analyses or on language features.

Nonetheless, at this point I believe that the current feature delivers value for Rust developers and fits the character of the Rust language overall.

@rfcbot reviewed

ohsayan, zslayton, tuguzT, ljedrz, utkarshgupta137, davidpdrsn, eldruin, SebastiaanYN, jackh726, marmeladema, and 53 more reacted with hooray emojicompiler-errors, darksv, ohsayan, zslayton, utkarshgupta137, tuguzT, davidpdrsn, SebastiaanYN, marmeladema, Progdrasil, and 44 more reacted with heart emojidarksv, ohsayan, zslayton, tuguzT, utkarshgupta137, davidpdrsn, eldruin, SebastiaanYN, TimNN, alexforster, and 33 more reacted with rocket emoji All reactions

rfcbot

added the final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. label

8 days ago

rfcbot commented 8 days ago

bellThis is now entering its final comment period, as per the review above. bell

ikenox, treysidechain, Mythra, daniel-noland, adriandelgado, npuichigo, Rageking8, oscartbeaumont, rodrigocfd, Cupnfish, and 5 more reacted with laugh emojialexforster, ohsayan, utkarshgupta137, SebastiaanYN, marjakm, Progdrasil, 71, jam1garner, i509VCB, Nilstrieb, and 156 more reacted with hooray emojiruniq, dnrusakov, amrhassan, phaazon, ohsayan, ikenox, treysidechain, MitchTurner, Mythra, emanguy, and 14 more reacted with heart emojibenluelo reacted with rocket emojijames7132, tiann, overlisted, runiq, Enet4, ohsayan, ikenox, treysidechain, Rageking8, oscartbeaumont, and 3 more reacted with eyes emoji All reactions

Contributor

zesterer commented 4 days ago

I've just encountered a minor bug with cyclical obligation resolution that I've not seen mentioned elsewhere in #101406 . It seems to be related to GATs, although I don't have the domain knowledge to confirm this. I'm personally of the view that it shouldn't block stabilisation (not that I have the authority to make such a call) given that it only seems to happen for code that would otherwise fail to compile anyway, but I thought it worth mentioning.

alice-i-cecile, oscartbeaumont, and changhe3 reacted with thumbs up emoji All reactions

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK