https://github.com/rust-lang/rust/pull/96709 Skip to content Toggle navigation Sign up * Product + Actions Automate any workflow + Packages Host and manage packages + Security Find and fix vulnerabilities + Codespaces Instant dev environments + Copilot Write better code with AI + Code review Manage code changes + Issues Plan and track work + Discussions Collaborate outside of code + Explore + All features + Documentation + GitHub Skills + Changelog * Solutions + By Size + Enterprise + Teams + Compare all + By Solution + CI/CD & Automation + DevOps + DevSecOps + Case Studies + Customer Stories + Resources * Open Source + GitHub Sponsors Fund open source developers + The ReadME Project GitHub community articles + Repositories + Topics + Trending + Collections * Pricing [ ] * # In this repository All GitHub | Jump to | * No suggested jump to results * # In this repository All GitHub | Jump to | * # In this organization All GitHub | Jump to | * # In this repository All GitHub | Jump to | Sign in Sign up {{ message }} rust-lang / rust Public * Notifications * Fork 9.7k * Star 71.7k * Code * Issues 5k+ * Pull requests 580 * Actions * Projects 0 * Security * Insights More * Code * Issues * Pull requests * Actions * Projects * Security * Insights 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. Pick a username [ ] Email Address [ ] Password [ ] [ ] Sign up for GitHub 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 Jump to bottom Stabilize generic associated types #96709 Merged bors merged 1 commit into rust-lang:master from jackh726: gats-stabilization Sep 13, 2022 Merged Stabilize generic associated types #96709 bors merged 1 commit into rust-lang:master from jackh726: gats-stabilization Sep 13, 2022 +313 -808 Conversation 185 Commits 1 Checks 10 Files changed 280 Conversation This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters Show hidden characters jackh726 Copy link Contributor @jackh726 jackh726 commented May 4, 2022 * edited Closes #44265 r? @nikomatsakis [?] Status of the discussion [?] * [*] There have been several serious concerns raised, summarized here. * [*] There has also been a deep-dive comment explaining some of the "patterns of code" that are enabled by GATs, based on use-cases posted to this thread or on the tracking issue. * [*] We have modeled some aspects of GATs in a-mir-formality to give better confidence in how they will be resolved in the future. You can read a write-up here. * [*] The major points of the discussion have been summarized on the GAT initiative repository. * [*] FCP has been proposed and we are awaiting final decisions and discussion amidst the relevant team members. 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.65 (2022-08-22 => beta, 2022-11-03 => stable). 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 = &'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>; 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 = & 'a ()>. However, you cannot currently write for X: Trait = T> or for X: Trait = [usize; N]>. Here is an example where this is needed: trait Foo {} trait Trait { type Assoc; } trait Trait2: Sized { fn foo = 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 = &'a ()>) {} //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ not allowed let ty: Box Trait = &'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. * Intervening type alias of GAT to causes incorrect E0581 #85533 * Typecheck fails when providing explicit types instead of GAT # 87803 In certain cases, we can run into cycle or overflow errors. This is more generally a problem with associated types. * Overflow evaluating the requirement with a where clause on associated type #87755 * Stall from overflow caused by for<'a> F: FnOnce(Self::Gat<'a>) # 87758 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. * Inconsistent treatment of different kinds of trait bounds with GATs #87831 * HRTB with GAT behaving differently in associated type bound vs where clause #90573 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. * Wrong (?) [E0631] signature mismatch for function with Fn trait arguments with GAT arguments #88382 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. * E0277 (bound not satisfied) for bound on generic associated types when combined with HRTB #88460 * Error typechecking lifetime-GAT bound when "parent" bound is inferred #96230 We don't fully have implied bounds for lifetimes appearing in GAT trait paths, which can lead to unconstrained type errors. * Introducing a GAT parameter leads to a false positive E0207 for another parameter constrained by Fn type #88526 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. * Wrong where Self: 'a GAT bound suggested #90816 * GAT Self: 'a bounds break some async blocks with impl Trait # 92096 * No syntactic way to specify GATs with where bounds in where clauses #95268 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. * lifetime bound not satisfied for a lending iterator (using generic associated types) with for_each method #91693 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. * Typecheck error when using a equality-constrained GAT in a trait. #91762 Some Iterator adapter patterns (namely filter) require Polonius or unsafe to work. * Filter adapter for LendingIterator requires Polonius #92985 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>; } fn foo() where T: Iterable, T::Item: 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 = &'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= &'a ()>. It might be that for<'a> is "too large" and it should instead be T: Trait 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 * On 2016-04-30, RFC opened * On 2017-09-02, RFC merged and tracking issue opened * On 2017-10-23, Move Generics from MethodSig to TraitItem and ImplItem * On 2017-12-01, Generic Associated Types Parsing & Name Resolution * On 2017-12-15, Lifetime Resolution for Generic Associated Types # 46706 * On 2018-04-23, Feature gate where clauses on associated types * On 2018-05-10, Extend tests for RFC1598 (GAT) * On 2018-05-24, Finish implementing GATs (Chalk) * On 2019-12-21, Make GATs less ICE-prone * On 2020-02-13, fix lifetime shadowing check in GATs * On 2020-06-20, Projection bound validation * On 2020-10-06, Separate projection bounds and predicates * On 2021-02-05, Generic associated types in trait paths * On 2021-02-06, Trait objects do not work with generic associated types * On 2021-04-28, Make traits with GATs not object safe * On 2021-05-11, Improve diagnostics for GATs * On 2021-07-16, Make GATs no longer an incomplete feature * On 2021-07-16, Replace associated item bound vars with placeholders when projecting * On 2021-07-26, GATs: Decide whether to have defaults for where Self: 'a * On 2021-08-25, Normalize projections under binders * On 2021-08-03, The push for GATs stabilization * On 2021-08-12, Detect stricter constraints on gats where clauses in impls vs trait * On 2021-09-20, Proposal: Change syntax of where clauses on type aliases * On 2021-11-06, Implementation of GATs outlives lint * On 2021-12-29. Parse and suggest moving where clauses after equals for type aliases * On 2022-01-15, Ignore static lifetimes for GATs outlives lint * On 2022-02-08, Don't constrain projection predicates with inference vars in GAT substs * On 2022-02-15, Rework GAT where clause check * On 2022-02-19, Only mark projection as ambiguous if GAT substs are constrained * On 2022-03-03, Support GATs in Rustdoc * On 2022-03-06, Change location of where clause on GATs * On 2022-05-04, A shiny future with GATs blog post * On 2022-05-04, Stabilization PR Sorry, something went wrong. 161 c410-f3r, ibraheemdev, terrarier2111, BoxyUwU, messense, fmease, marmeladema, Stumblinbear, Kobzol, mental32, and 151 more reacted with thumbs up emoji 182 c410-f3r, ibraheemdev, terrarier2111, BoxyUwU, lcnr, jhpratt, messense, ickk, fmease, naim94a, and 172 more reacted with hooray emoji [?] 147 c410-f3r, ibraheemdev, terrarier2111, xd009642, BoxyUwU, lcnr, messense, fmease, aliemjay, marmeladema, and 137 more reacted with heart emoji 118 c410-f3r, ibraheemdev, terrarier2111, BoxyUwU, darksv, messense, jakobhellermann, fmease, aliemjay, marmeladema, and 108 more reacted with rocket emoji 20 Dirbaio, SirCharlieMars, overlisted, weihanglo, Veetaha, remi-dupre, runiq, AngelOnFira, johnyenter-briars, proudmuslim-dev, and 10 more reacted with eyes emoji All reactions * 161 reactions * 182 reactions * [?] 147 reactions * 118 reactions * 20 reactions @rustbot 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 May 4, 2022 @rust-highfive rust-highfive assigned nikomatsakis May 4, 2022 @rust-highfive Copy link Collaborator rust-highfive commented May 4, 2022 Some changes occurred in src/tools/rustfmt. cc @rust-lang/rustfmt 2 calebcartwright and schneiderfelipe reacted with thumbs up emoji All reactions * 2 reactions Sorry, something went wrong. @rust-highfive rust-highfive added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label May 4, 2022 @c410-f3r Copy link Contributor c410-f3r commented May 4, 2022 Thank you @jackh726. Like, really, thank you very much! [?] 70 jackh726, aliemjay, Kobzol, Nilstrieb, conradludgate, Dengjianping, jamesmunns, kellerkindt, bryanhitc, jdahlstrom, and 60 more reacted with heart emoji All reactions * [?] 70 reactions Sorry, something went wrong. @jackh726 jackh726 force-pushed the gats-stabilization branch from d688e42 to 7e6e507 Compare May 4, 2022 @jackh726 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 May 4, 2022 @bors Copy link Contributor bors commented May 5, 2022 [?] The latest upstream changes (presumably #96593) made this pull request unmergeable. Please resolve the merge conflicts. All reactions Sorry, something went wrong. @nrc Copy link Member nrc commented May 5, 2022 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. 63 mejrs, petervaro, afetisov, oslac, mattias-p, PhilipDaniels, overlisted, Shamazo, hawkingrei, BurntSushi, and 53 more reacted with thumbs up emoji 74 surban, marmeladema, c410-f3r, cjwcommuny, Progdrasil, TennyZhuang, CraftSpider, phaazon, fakeshadow, Dirbaio, and 64 more reacted with thumbs down emoji 23 yasammez, andreytkachenko, marmeladema, TennyZhuang, demurgos, Globidev, johnyenter-briars, QnnOkabayashi, ivan770, gtsiam, and 13 more reacted with confused emoji [?] 20 petervaro, overlisted, mominul, Systemcluster, phaylon, ldanko, ChayimFriedman2, vrmiguel, dhardy, j-hc, and 10 more reacted with heart emoji 16 rubdos, darksv, slanterns, Enet4, runiq, tshepang, johnyenter-briars, Xunjin, yct21, CGMossa, and 6 more reacted with eyes emoji All reactions * 63 reactions * 74 reactions * 23 reactions * [?] 20 reactions * 16 reactions Sorry, something went wrong. @zesterer Copy link Contributor zesterer commented May 5, 2022 * 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. 81 marmeladema, c410-f3r, cjwcommuny, rrbutani, slanterns, therewillbecode, Progdrasil, TennyZhuang, Xuanwo, bstrie, and 71 more reacted with thumbs up emoji 1 ChayimFriedman2 reacted with confused emoji [?] 23 jam1garner, AngelOnFira, bew, johnyenter-briars, QnnOkabayashi, praveenperera, gtsiam, CGMossa, tux3, trevyn, and 13 more reacted with heart emoji 9 Enet4, runiq, overlisted, tshepang, Timmmm, gtsiam, Ayawen01, kotatsuyaki, and lassipulkkinen reacted with eyes emoji All reactions * 81 reactions * 1 reaction * [?] 23 reactions * 9 reactions Sorry, something went wrong. @NobodyXu Copy link Contributor NobodyXu commented May 5, 2022 @zesterer Can you please explain on how you use GAT to speedup your parser? 6 jjpe, MavethGH, SirCharlieMars, schneiderfelipe, bryanhitc, and alfatm reacted with thumbs up emoji All reactions * 6 reactions Sorry, something went wrong. @zesterer Copy link Contributor zesterer commented May 5, 2022 * 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. 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, 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 Vecs). 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. 57 c410-f3r, rrbutani, slanterns, mattias-p, tinaun, Progdrasil, TennyZhuang, NobodyXu, bstrie, zslayton, and 47 more reacted with thumbs up emoji [?] 23 jam1garner, AngelOnFira, Globidev, bew, GoldsteinE, numToStr, raftario, gtsiam, tux3, jkugelman, and 13 more reacted with heart emoji 8 bew, ChayimFriedman2, nasso, Stumblinbear, Kobzol, schneiderfelipe, abbudao, and alfatm reacted with rocket emoji All reactions * 57 reactions * [?] 23 reactions * 8 reactions Sorry, something went wrong. @overlisted Copy link overlisted commented May 5, 2022 @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. All reactions Sorry, something went wrong. @tinaun Copy link Contributor tinaun commented May 5, 2022 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 2 jplatte and schneiderfelipe reacted with thumbs up emoji 6 Kolsky, james7132, TheRawMeatball, SirCharlieMars, ColonelThirtyTwo, and optozorax reacted with thumbs down emoji 5 overlisted, Progdrasil, ibraheemdev, Kolsky, and james7132 reacted with confused emoji All reactions * 2 reactions * 6 reactions * 5 reactions Sorry, something went wrong. @BurntSushi Copy link Member BurntSushi commented May 5, 2022 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. 48 afetisov, Enet4, NobodyXu, jbuckmccready, ChayimFriedman2, Michael-F-Bryan, YaLTeR, jplatte, Timmmm, clintfred, and 38 more reacted with thumbs up emoji 2 c410-f3r and Globidev reacted with confused emoji [?] 12 Systemcluster, runiq, mominul, golddranks, CGMossa, tux3, HeroicKatora, Ayawen01, eminence, SirCharlieMars, and 2 more reacted with heart emoji All reactions * 48 reactions * 2 reactions * [?] 12 reactions Sorry, something went wrong. @jam1garner Copy link Contributor jam1garner commented May 5, 2022 * edited In response to @nrc (and making every section collapsible to not take up 2 vertical screen widths): "GATs nearly always increase complexity" GATs nearly always increase complexity. They introduce new syntax and new semantics to the type system. I've always found this argument weird. To me GATs is the perfect example of a "simplification" feature. This is not because GATs are easy to reason about, but rather because I see the lack of them as an inconsistency in the language in its present state. If I have the following code: type Result = core::result::Result<(), Error>; trait Iterator { type Item; } To me, there is a very intentional parallel here, as ultimately an associated type is just a type alias with a specific purpose. If you really boil them down, sure, the late-binding nature of type aliases make them function a bit differently. But at the end of the day, an associated type is really just an eagerly type-checked type-alias as a part of a trait's interface. So to me the following: type Result = core::result::Result; trait Iterator { type Item<'a>; } Is the same feature just more consistent between the kinds of type-alias. This is possibly an unfair comparison due to the fact one has eager trait bounds and the other is evaluated at use-time, and thus has different type-system vibes. But honestly, the first time I tried to add a generic bound to an associated type years ago, I remember my experience was: 1. "wait that doesn't work?" 2. struggling to find the right keywords to google 3. "wait THAT'S what people mean by GATs? I thought it was some wizardry" To me, this is what separates it from non-lifetime HRTBs: it is a natural extension of existing ideas, not a true introduction of genericism over an additional dimension. GATs' complexity is in the public API 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 Honestly not sure I agree with this. For example serde's Deserialize trait: trait Deserialize<'de>: Sized { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>; } This uses: trait-level lifetime generics, associated types, a generic type, and a method-level where clause which ties a trait-level generic lifetime as a generic parameter in a trait bound. The thing is... I've seen countless new users not only use serde but enjoy it quite a bit, far before they can even parse the syntax of the above. Because honestly? you don't need to understand 90% of a generic API, especially if it is well-documented. A beginner only needs to care about one thing: "does my type implement Deserialize"? serde couldn't exist in its current form without the decently complex capabilities present above, and I highly doubt anyone is arguing serde is an example of Rust being too complex or overly academic. Rust's Iterator APIs are arguably just as bad with their use of Fn traits, associated types with trait bounds, wrapper types, etc. But in practice, do people have much trouble with them? or even think about the generic bounds much if they aren't writing their own combinator or highly-generic code? I don't personally, and I feel I haven't seen this behavior from any of the beginners I've regularly worked with. In my experience, the reality is these powerful features only end up "in the APIs" in a stability sense. Users just don't need to care about them until things break. And when things break, rustc solves for why and gives excellent diagnostics. It tells you "Deserialize isn't implemented" and then you never cared about the specifics of the trait bounds, for even a second. (albeit I would definitely say work in allowing library authors to provide domain-specific insight would make this so much better) I would go as far as to say this makes things easier on the end user, not harder. "if a programmer is not doing async programming, they don't need to know about async or await" 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). 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". And since there exists no good way (yet) to provide an API that can be synchronous or asynchronous, a good portion of those libraries just expose an async-only API (Think crates for interacting with web APIs). It's not super pervasive, but I'd need more than one hand to count the number of projects where I've had to introduce async unnecessarily. I honestly personally think this aids your point, but I figured it was pointing out regardless. "Haskell is [...] largely shunned by industry" Note how popular Haskell is with PL/compiler people, but how it is largely shunned by industry. I think it's far from fair to try and imply a single aspect of Haskell (the degree of abstraction) is the sole cause. Honestly it would be incredibly impressive if Haskell was more popular given everything about it. It's functional, its syntax is far from any language that already has a foothold in industry, it doesn't really have a "killer feature" (its primary feature is just its core language design as a general-purpose language), it doesn't have a specific niche it's the go-to for. Consider a language which is quite popular in industry, and about as far as possible from Haskell: Go. It has a niche (web services/backend/devops/etc), it has a "killer feature" (goroutines), it has a corporate backing (Google), it is abundantly uninterested in introducing anything but the most mainstream of language/syntax design, etc. All of these are important factors for adoption! As is being a simple language is likely a big factor too of course, but that's my point: Go's success doesn't exist in a vacuum of its simplicity, and Haskell's lack of industry permeation and ability to form complex abstractions doesn't exist in a vacuum either. I would say, in a lot of ways, Rust has brought in a lot of ideas that (at least at the time of their introduction to the language) would fall into the "only exists in 'academic' languages". Things like immutability, type inference, sum types, pattern matching, typeclasses, etc are all feature where Rust has had great success taking great ideas from less-successful languages and tried to fit them into a design that is more familiar and . The joke "Rust is a gateway drug to Haskell" is, imo, one of Rust's biggest compliments, as to me it speaks to Rust's ability to bring some of the best parts of PL design to a more accessible language with a design that enables a higher degree of practical usage (namely feature like FFI and flexibility regarding where it can be used). --------------------------------------------------------------------- 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" we have found many times that abstractions like 'collection' or 'number' are not good fits for most libraries 100% agreed. Recent discussions have seen quite a bit of agreement that both of those are too general (albeit I would say large subsets of the idea of "number" are useful depending on the context). I would say especially there doesn't seem to be many experienced Rust users in favor of collection traits/abstractions. I would actually hate to see that, frankly. However, just because GATs would enable this style of abstraction more doesn't mean we have to make that idiomatic. And you might be asking why I'd say to add a feature but not use it, as frankly I imagine that is what it reads like. Personally I don't really think this is where GATs shine? And that leads right into my next point.... "Most use cases [...] are for lifetime GATs" Most use cases (and certainly most of the compelling use cases) are for lifetime GATs You are.... completely right here. I have occasionally needed type GATs here and there, but I feel very limited by the lack of lifetime bounds. Whether your conclusion (stabilize type GATs separately) is the direction we should go, I have less of an opinion on. I think for a bit of context for those who aren't familiar, or a concrete example for those who are, if we have a trait like such: trait CanDoThing { type Input; fn do_thing(&self, input: Self::Input); } We have a bit of a fundamental limitation: there is no way for us to tie the lifetime of Input to the length of do_thing. So despite the fact do_thing might even guarantee the input isn't long-lived, it isn't possible to specify that such that a user can even declare a borrowed Input type. The solution with GATs is just to give Input a lifetime and then we can specify that Input doesn't live past the end of do_thing: trait CanDoThing { type Input<'a>; fn do_thing(&self, input: Self::Input<'_>); } One reason this is really useful is allowing composable traits (think serde: if you can serialize all the fields, you can serialize the struct) to have user-provided types without requiring either reference counting or passing by value. For declarative APIs (serde, clap/structopt, etc) this is a really useful feature. It's technically possible to emulate lifetime GATs here using HRTB trickery to make a lifetime bound out of thin air, but it has significant limitations and is prohibitively messy. --------------------------------------------------------------------- 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 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, it's "there are papercuts and I am not in the loop enough to assert the fixes would be backwards-compatible". 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? Are there papercuts which allowing upper bound constraints on HRTB lifetimes (relevant snippet from Sabrina's blog post below) don't cover? The ultimate conclusion of all this is that HRTBs basically can't be used with lifetime GATs at all. for<'a> just doesn't express the right requirement -- we don't want to require the bound for any lifetime, we only really want to require it for lifetimes shorter than '0. Ideally, we would be able to write in a where clause there, so the bounds of print_items could become: fn print_items(mut iter: I) where I: LendingIterator, for<'a where I: 'a> I::Item<'a>: Debug, 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. 51 c410-f3r, zesterer, overlisted, NobodyXu, dgiger42, slanterns, Xuanwo, demurgos, rrbutani, Progdrasil, and 41 more reacted with thumbs up emoji [?] 25 zesterer, overlisted, bstrie, runiq, rrbutani, tshepang, Progdrasil, wesleywiser, ChayimFriedman2, Drakulix, and 15 more reacted with heart emoji All reactions * 51 reactions * [?] 25 reactions Sorry, something went wrong. @phaazon Copy link phaazon commented May 5, 2022 * 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). 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). 12 overlisted, Enet4, zesterer, NobodyXu, Progdrasil, Globidev, Pzixel, gtsiam, recatek, 8573, and 2 more reacted with thumbs up emoji All reactions * 12 reactions Sorry, something went wrong. @Stargateur Copy link Contributor Stargateur commented May 5, 2022 * 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? 2 Emerentius and kadiwa4 reacted with thumbs up emoji 3 CGMossa, Stumblinbear, and kekeimiku reacted with thumbs down emoji 1 phaazon reacted with confused emoji All reactions * 2 reactions * 3 reactions * 1 reaction Sorry, something went wrong. @BurntSushi Copy link Member BurntSushi commented May 5, 2022 * 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.) 37 ChayimFriedman2, Enet4, sthiele, YaLTeR, kellerkindt, jplatte, PoignardAzur, eldruin, vi, raftario, and 27 more reacted with thumbs up emoji 1 c410-f3r reacted with confused emoji [?] 25 jam1garner, Keats, afetisov, runiq, inquisitivecrystal, mominul, faptc, andylizi, jedel1043, bew, and 15 more reacted with heart emoji All reactions * 37 reactions * 1 reaction * [?] 25 reactions Sorry, something went wrong. @Stargateur Copy link Contributor Stargateur commented May 5, 2022 * 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 2 c410-f3r and CGMossa reacted with thumbs up emoji 2 c410-f3r and CGMossa reacted with rocket emoji All reactions * 2 reactions * 2 reactions Sorry, something went wrong. @BurntSushi Copy link Member BurntSushi commented May 5, 2022 @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. 18 afetisov, jbuckmccready, faptc, jedel1043, ChayimFriedman2, YaLTeR, PoignardAzur, raftario, burjui, tux3, and 8 more reacted with thumbs up emoji All reactions * 18 reactions Sorry, something went wrong. @jam1garner Copy link Contributor jam1garner commented May 5, 2022 @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 [?] 9 BurntSushi, bstrie, rrbutani, Progdrasil, CGMossa, gz, JoJoJet, SirCharlieMars, and abbudao reacted with heart emoji All reactions * [?] 9 reactions Sorry, something went wrong. @slanterns Copy link Contributor slanterns commented May 5, 2022 * edited it's far from fair to try and imply a single aspect of Haskell (the degree of abstraction) is the sole cause... I think it's far from fair to try and imply a single aspect of Haskell (the degree of abstraction) is the sole cause. Honestly it would be incredibly impressive if Haskell was more popular given everything about it. It's functional, its syntax is far from any language that already has a foothold in industry, it doesn't really have a "killer feature" (its primary feature is just its core language design as a general-purpose language), it doesn't have a specific niche it's the go-to for. Consider a language which is quite popular in industry, and about as far as possible from Haskell: Go. It has a niche (web services/backend/ devops/etc), it has a "killer feature" (goroutines), it has a corporate backing (Google), it is abundantly uninterested in introducing anything but the most mainstream of language/syntax design, etc. All of these are important factors for adoption! As is being a simple language is likely a big factor too of course, but that's my point: Go's success doesn't exist in a vacuum of its simplicity, and Haskell's lack of industry permeation and ability to form complex abstractions doesn't exist in a vacuum either. I would say, in a lot of ways, Rust has brought in a lot of ideas that (at least at the time of their introduction to the language) would fall into the "only exists in 'academic' languages". Things like immutability, type inference, sum types, pattern matching, typeclasses, etc are all feature where Rust has had great success taking great ideas from less-successful languages and tried to fit them into a design that is more familiar and . The joke "Rust is a gateway drug to Haskell" is, imo, one of Rust's biggest compliments, as to me it speaks to Rust's ability to bring some of the best parts of PL design to a more accessible language with a design that enables a higher degree of practical usage (namely feature like FFI and flexibility regarding where it can be used). 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! 4 8573, gtsiam, JoJoJet, and SirCharlieMars reacted with thumbs up emoji All reactions * 4 reactions Sorry, something went wrong. @CraftSpider Copy link Contributor CraftSpider commented May 5, 2022 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. 30 overlisted, marmeladema, jedel1043, tshepang, Globidev, demurgos, malthesr, zesterer, Progdrasil, dgiger42, and 20 more reacted with thumbs up emoji All reactions * 30 reactions Sorry, something went wrong. @Stargateur Copy link Contributor Stargateur commented May 6, 2022 * 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. 3 overlisted, bew, and SirCharlieMars reacted with eyes emoji All reactions * 3 reactions Sorry, something went wrong. @nrc Copy link Member nrc commented May 6, 2022 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. 16 Enet4, zesterer, NobodyXu, Globidev, tshepang, ChayimFriedman2, raftario, tux3, Virgiel, EdorianDark, and 6 more reacted with thumbs up emoji All reactions * 16 reactions Sorry, something went wrong. @kellerkindt Copy link Contributor kellerkindt commented May 6, 2022 * 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! 8 NobodyXu, overlisted, ChayimFriedman2, willcrozi, JoJoJet, SirCharlieMars, bryanhitc, and kraktus reacted with thumbs up emoji [?] 3 Globidev, tmandry, and SirCharlieMars reacted with heart emoji All reactions * 8 reactions * [?] 3 reactions Sorry, something went wrong. @PoignardAzur Copy link Contributor PoignardAzur commented May 6, 2022 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<>::Item>; } All reactions Sorry, something went wrong. 182 hidden items Load more... @joshtriplett Copy link Member joshtriplett commented Sep 8, 2022 @rfcbot concern rust-reference-pr @ehuss noted that this doesn't have a PR to the Rust Reference documenting it, which we've been requiring before stabilizing other features. 3 compiler-errors, andreytkachenko, and kellytk reacted with thumbs up emoji 3 c410-f3r, gtsiam, and slanterns reacted with laugh emoji 2 eggyal and anlihust reacted with confused emoji All reactions * 3 reactions * 3 reactions * 2 reactions Sorry, something went wrong. @rfcbot rfcbot added proposed-final-comment-period Proposed to merge/ close by relevant subteam, see T- label. Will enter FCP once signed off. and removed final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. labels Sep 8, 2022 @vacuus vacuus mentioned this pull request Sep 9, 2022 [WIP] make nightly compilers able to parallelize #101566 Open @jackh726 Copy link Contributor Author jackh726 commented Sep 9, 2022 @joshtriplett I've opened rust-lang/reference#1265. 10 matthiasbeyer, compiler-errors, 71, andreytkachenko, ohsayan, joshtriplett, Rageking8, makroiss, dnrusakov, and zohnannor reacted with thumbs up emoji [?] 10 compiler-errors, ohsayan, 71, zesterer, kjetilkjeka, jjpe, Rageking8, makroiss, dnrusakov, and zohnannor reacted with heart emoji All reactions * 10 reactions * [?] 10 reactions Sorry, something went wrong. @ohsayan ohsayan mentioned this pull request Sep 9, 2022 Internal: Rewrite internal core structures skytable/skytable#247 Open 6 tasks @joshtriplett Copy link Member joshtriplett commented Sep 9, 2022 @rfcbot resolved rust-reference-pr 5 ohsayan, Aloso, adriandelgado, Rageking8, and ghishadow reacted with thumbs up emoji 5 ohsayan, adriandelgado, kellytk, Rageking8, and harudagondi reacted with hooray emoji [?] 3 ohsayan, Rageking8, and ghishadow reacted with heart emoji 6 ohsayan, ilslv, faern, adriandelgado, Rageking8, and ghishadow reacted with rocket emoji 3 ohsayan, Rageking8, and ghishadow reacted with eyes emoji All reactions * 5 reactions * 5 reactions * [?] 3 reactions * 6 reactions * 3 reactions Sorry, something went wrong. @rfcbot rfcbot added the final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. label Sep 9, 2022 @rfcbot Copy link rfcbot commented Sep 9, 2022 This is now entering its final comment period, as per the review above. 12 Rageking8, ikenox, ohsayan, adriandelgado, dnrusakov, schneiderfelipe, TaKO8Ki, treysidechain, CertainLach, LiHRaM, and 2 more reacted with thumbs up emoji 53 ohsayan, biro456, DGolubets, attila-lin, ljedrz, liquidev, jontze, alice-i-cecile, Uriopass, tiann, and 43 more reacted with hooray emoji 2 j-hc and glebpom reacted with confused emoji [?] 12 Rageking8, overlisted, yoshuawuyts, ikenox, ohsayan, dnrusakov, schneiderfelipe, treysidechain, CertainLach, ms-jpq, and 2 more reacted with heart emoji 19 ohsayan, biro456, rami3l, Progdrasil, 12101111, Uriopass, overlisted, Globidev, yoshuawuyts, ikenox, and 9 more reacted with rocket emoji 6 Rageking8, Enet4, ikenox, kellytk, treysidechain, and kupiakos reacted with eyes emoji All reactions * 12 reactions * 53 reactions * 2 reactions * [?] 12 reactions * 19 reactions * 6 reactions Sorry, something went wrong. @rfcbot rfcbot removed the proposed-final-comment-period Proposed to merge/close by relevant subteam, see T- label. Will enter FCP once signed off. label Sep 9, 2022 @ohsayan ohsayan mentioned this pull request Sep 10, 2022 Internal: Reduce type dependency on engines for easy swapping (Part 1) skytable/skytable#282 Closed @jackh726 Copy link Contributor Author jackh726 commented Sep 12, 2022 Okay it's time. It's day 12 of the original FCP. Given the transient concern of the second and the fact that ideally this gets in before Friday, I'm going to go ahead with the merge for this (mentioned on zulip https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/ topic/ rfcbot.20should.20support.20pause.20fcp.20instead.20of.20aborting.20it.2E.2E.2E /near/298062751). The reference PR is up and I'll work throughout the week to make sure that gets merged (another reason not to wake, since this blocks that merge). Thanks everyone for the discussion here. @bors r=compiler-errors 6 Rageking8, treysidechain, ohsayan, james7132, dnrusakov, and msrd0 reacted with thumbs up emoji 97 benschulz, m-rots, kellerkindt, vultix, Mathspy, hasali19, SergeyKasmy, davidpdrsn, praveenperera, tuguzT, and 87 more reacted with hooray emoji [?] 20 robinhundt, conradludgate, Kolsky, Nessex, Rageking8, Nilstrieb, clubby789, slanterns, WaffleLapkin, treysidechain, and 10 more reacted with heart emoji All reactions * 6 reactions * 97 reactions * [?] 20 reactions Sorry, something went wrong. @bors Copy link Contributor bors commented Sep 12, 2022 Commit 3cf0e98 has been approved by compiler-errors It is now in the queue for this repository. All reactions Sorry, something went wrong. @bors Copy link Contributor bors commented Sep 12, 2022 The tree is currently closed for pull requests below priority 1000. This pull request will be tested once the tree is reopened. All reactions Sorry, something went wrong. @bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Sep 12, 2022 @eggyal Copy link Contributor eggyal commented Sep 12, 2022 Despite being a complete nobody within the Rust world, this is such a momentous occasion for the language that I'd really like to offer a huge note of thanks to everyone involved for the Herculean effort you've put into this feature and now getting stabilisation over the line! Although I have a pretty good idea who some of the key people have been, I've not been involved in any way and I fear that name-checking some individuals will undoubtedly do a disservice to the many other contributors I'll undoubtedly (and unfairly) omit. Nevertheless, for this stabilisation issue, I will stick out my neck to particularly thank @jackh726, @nikomatsakis and @compiler-errors for their push/ contributions/review; the various team members who all gave stabilisation its due careful consideration; and of course everyone who offered their thoroughly enlightening and useful views. 7 runiq, NobodyXu, FinnPerry, Kixunil, phase, NicholasSterling, and kellytk reacted with thumbs up emoji [?] 72 Xiretza, tuguzT, Rexagon, andreytkachenko, slanterns, cynecx, zslayton, Rageking8, solomatov, dbanty, and 62 more reacted with heart emoji All reactions * 7 reactions * [?] 72 reactions Sorry, something went wrong. @Dylan-DPC Copy link Member Dylan-DPC commented Sep 13, 2022 @bors rollup=never 6 slanterns, ohsayan, runiq, rye, whentze, and phase reacted with laugh emoji 3 jhpratt, thvdveld, and ohsayan reacted with rocket emoji 2 jplatte and Systemcluster reacted with eyes emoji All reactions * 6 reactions * 3 reactions * 2 reactions Sorry, something went wrong. @bors Copy link Contributor bors commented Sep 13, 2022 [?] Testing commit 3cf0e98 with merge 7098c18... 22 ohsayan, TmLev, obmarg, thvdveld, davidpdrsn, Xazax-hun, conradludgate, glebpom, andresovela, Nessex, and 12 more reacted with rocket emoji All reactions * 22 reactions Sorry, something went wrong. @bors Copy link Contributor bors commented Sep 13, 2022 [?] Test successful - checks-actions Approved by: compiler-errors Pushing 7098c18 to master... 1 kellytk reacted with thumbs up emoji 174 slanterns, bugadani, Evian-Zhang, conradludgate, darksv, obmarg, andresovela, CYBAI, yvt, ilslv, and 164 more reacted with hooray emoji 24 overlisted, benluelo, ikenox, weihanglo, AlecGoncharow, james7132, zohnannor, dnsco, Carter0, zbraniecki, and 14 more reacted with rocket emoji All reactions * 1 reaction * 174 reactions * 24 reactions Sorry, something went wrong. @bors bors added the merged-by-bors This PR was explicitly merged by bors label Sep 13, 2022 Hide details View details @bors bors merged commit 7098c18 into rust-lang:master Sep 13, 2022 11 checks passed @rustbot rustbot added this to the 1.65.0 milestone Sep 13, 2022 This was referenced Sep 13, 2022 Neither require nor imply lifetime bounds on opaque type for well formedness #95474 Open emit ProjectionPredicate obligations when relating projections #96912 Open Make Sized coinductive, again #100386 Open @rust-timer Copy link Collaborator rust-timer commented Sep 13, 2022 Finished benchmarking commit (7098c18): comparison URL. Overall result: no relevant changes - no action needed @rustbot label: -perf-regression Instruction count This benchmark run did not return any relevant results for this metric. Max RSS (memory usage) Results This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment. mean^1 range count^2 Regressions 5.4% [5.4%, 5.4%] 1 (primary) Regressions 1.8% [1.8%, 1.8%] 1 (secondary) Improvements -2.6% [-2.6%, -2.6%] 1 (primary) Improvements -2.3% [-2.7%, -1.6%] 3 (secondary) All (primary) 1.4% [-2.6%, 5.4%] 2 Cycles Results This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment. mean^1 range count^2 Regressions 2.4% [1.7%, 3.0%] 2 (primary) Regressions - - 0 (secondary) Improvements -2.9% [-2.9%, -2.9%] 1 (primary) Improvements - - 0 (secondary) All (primary) 0.6% [-2.9%, 3.0%] 3 Footnotes 1. the arithmetic mean of the percent change - -^2 2. number of relevant changes - -^2 All reactions Sorry, something went wrong. @jackh726 jackh726 deleted the gats-stabilization branch Sep 13, 2022 @crlf0710 crlf0710 added the relnotes Marks issues that should be documented in the release notes of the next release. label Sep 13, 2022 @ahl ahl mentioned this pull request Sep 13, 2022 new area of focus with rust-lang/rust#96709 merged? rust-lang/ generic-associated-types-initiative#9 Open Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment Reviewers @compiler-errors compiler-errors Assignees @nikomatsakis nikomatsakis Labels A-rustdoc-json Area: Rustdoc JSON backend disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. F-generic_associated_types `#![feature(generic_associated_types)]` a.k.a. GATs final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. merged-by-bors This PR was explicitly merged by bors relnotes Marks issues that should be documented in the release notes of the next release. S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. S-waiting-on-fcp Status: PR is in FCP and is awaiting for FCP to complete. T-lang Relevant to the language team, which will review and decide on the PR/issue. T-types Relevant to the types team, which will review and decide on the PR/issue. Projects None yet Milestone 1.65.0 Development Successfully merging this pull request may close these issues. Tracking issue for generic associated types (GAT) 77 participants @jackh726 @rust-highfive @c410-f3r @bors @nrc @zesterer @NobodyXu @overlisted @tinaun @BurntSushi @jam1garner @phaazon @Stargateur @slanterns @CraftSpider @kellerkindt @PoignardAzur @audunhalland @afetisov @QuineDot @clintfred @Pzixel @i509VCB @MomoLangenstein @ChayimFriedman2 @dimpolo @inquisitivecrystal @ZoopOTheGoop @stijnfrishert @nikomatsakis @CLEckhardt @mijamo @HindrikStegenga @benschulz @eggyal @orsenkucher @withoutboats @comex @pythonesque @andreytkachenko @danaugrs @MabezDev @tschuett @ryzhyk @james7132 @TimNN @Enet4 @grenewode @Evian-Zhang @leo60228 @TennyZhuang @traviscross @bstrie @kafji @vultix @evenyag @skyzh @sundy-li @compiler-errors @udoprog @rfcbot @BoxyUwU @dhardy @jgarvin @TheDan64 @vorot93 @ssokolow @joshtriplett @PureWhiteWu @ohsayan @est31 @rustbot @rbtcollins @pnkfelix @Dylan-DPC @rust-timer @crlf0710 Add this suggestion to a batch that can be applied as a single commit. This suggestion is invalid because no changes were made to the code. Suggestions cannot be applied while the pull request is closed. Suggestions cannot be applied while viewing a subset of changes. Only one suggestion per line can be applied in a batch. Add this suggestion to a batch that can be applied as a single commit. Applying suggestions on deleted lines is not supported. You must change the existing code in this line in order to create a valid suggestion. Outdated suggestions cannot be applied. This suggestion has been applied or marked resolved. Suggestions cannot be applied from pending reviews. Suggestions cannot be applied on multi-line comments. Suggestions cannot be applied while the pull request is queued to merge. Footer (c) 2022 GitHub, Inc. Footer navigation * Terms * Privacy * Security * Status * Docs * Contact GitHub * Pricing * API * Training * Blog * About You can't perform that action at this time. You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.