GATs permit complex patterns that will, on net, make Rust harder to use


There is no question that GATs enable new design patterns. For the most part, these design patterns take the form of enabling a new kind of abstraction. For example, many modes allows a trait that encapsulates a "mode" in which some other code will be executed; lacking GATs, this can only be done in simple cases. These new design patterns may locally improve the code, but, if it becomes commonplace to use more complex abstractions in Rust code bases, Rust code overall will become harder for people to understand and read. As burntsushi wrote:

The complexity increase comes precisely from the abstraction power given to folks. By increasing the accessibility to that power, that power will in turn be used more frequently and users of the language will need to contend with those abstractions.

burntsushi continues (emphasis ours):

Here's where we come to the contentious part of this perspective: a lot of people have real difficulty understanding abstractions.

Nick Cameron also wrote a blog post covering this theme. One of Nick's comments on Zulip points out that duplicated code in an API surface is annoying to maintain, but could be easier for consumers:

Nick Cameron: I think the risk of features like GATs is that encourages smaller, more abstract APIs which are more satisfying for library authors but which are harder for the median user to comprehend. For a non-GAT example, consider Result and Option compared to a more abstract monadic type. The latter reduces code duplication in the library and perhaps permits more interoperability, but we prefer having separate Result and Option in Rust because for most users, it's easier to understand an API with concrete values and functions like Ok and unwrap, rather than abstract concepts like unit and bind.

What are the alternatives?

Nobody is contending that the design patterns aren't addressing real problems, but they are contending that solving those problems with a trait-based abstraction may not be the right approach. So what are these alternatives?

  • In some cases, it's a "slightly less convenient API" (as in CSV reader).
  • In others, the answer is some combination of macros and code generation. For example, the many modes pattern was used to make parser combinators work better, but that could also be addressed via procedural macros and code generation.
  • In yet others, it's HRTB; the standard workaround for the Iterable pattern is to write for<'c> Iterate<'c>, for example.
  • Finally, it is sometimes possible to workaround the lack of GATs by introducing runtime overhead. e.g., foregoing the optimizations that many modes enabled, or using an enum, or a trait object.

Counterpoint: GATs often allow library authors to hide complexity

Somewhat counter the above, frequently the goal with GATs is actually to make the interface of the library simpler:

oli: Yes, and in user libraries right now you do stumble across those very complex APIs that work around the lack of GATs

vultix: I've personally seen two use-cases where GATs would have been useful, both times while writing libraries. We found an ugly workaround for the first use-case that wasn't horrible, but the second workaround made the user-facing api severely worse.

almann: Currently, delegation via our current APIs are difficult because we cannot have an associated type that can be constructed with a per invocation lifetime. This example illustrates this, and shows how we can work around this by the judicious use of unsafe code, by essentially emulating the reference with pointers we know to be scoped appropriately. We also work around the lack of GAT by simplifying the value writer used in the implementation of IonCAnnotationsFieldWriter for IonCWriterHandle by refering to Self which works, but makes the API potentially easier to misuse.

The many modes pattern shows how the Chumsky parser generator used GATs internally. They don't appear in the public facing methods.

Jake Goulding points out that we hear very few stories of people who tried GATs but backed off:

Jake Goulding: It would be wonderful if there were experience reports of people saying "I thought I needed GATs but after writing them I realized I could do X instead and that was much clearer". My one experience was that I could use one of the stable GAT workarounds, but it felt wrong and I was happy to trial-run the nightly GAT version instead.

Counterpoint: Macros are not easier

Many folks chimed in to express their feeling that proc macros, or duplicated code, are not easier to understand or maintain. Some examples:

Alice Cecile: Macros are so much worse to read / write / maintain than pretty much any type magic I've ever seen. Even very simple stuff is rough

Ralf Jung: I would not call this kind of nonsense very clear. macros do tend to lead to state explosion and huge amounts of redundancy in the docs, making it very hard to see the actual pattern. that's unsurprising since the compiler is never told about the pattern. (that particular example is not solved by GATs, it requires other new type system features, including variadics. it just demonstrates well the perils of macro-generated code.)

Counterpoint: Arguing against a feature because it could be misused would block many parts of Rust

Ralf Jung: yes, that I think is the main argument to me. not having a feature because it could be used to write unnecessarily complicated APIs sounds like a bad argument to me -- it effectively means preventing some people from writing good APIs (assuming we accept there are APIs where the GAT version is clearly superior) for fear of other people writing bad APIs. one can already write unnecessarily complicated APIs in Rust in a lot of ways -- e.g. we didnt block proc-macros just because they can be used for bad APIs, though anyone who had to debug other people's sea of macros knows it can easily lead to terribly opaque API surfaces. heck, we have unsafe code, where the consequences of using the feature in the wrong way are (IMO) a lot worse than GATs. I dont understand why GATs are suddenly considered a problem when they are (IMO) a feature much less in danger of being misused than unsafe code or proc macros. Rust has empowerment literally in its one-sentence slurb: we give people powerful tools and all the help we can to use them, and we accept that this means some people will misuse them, and we do what we can (technically and socially) to mitigate the consequences of that.