The entire point of GATs was to carve out a design space that solved peoples' problems without being so high-minded as monads and HKT and so on. Unfortunately, a lot of people who like monads like to talk about GATs in the same way. But its really as simple as this: when you add an associated type to a trait, you can make that type generic, the same way you can make a type alias generic when its not in a trait. The whole point of specifying GATs and not a more general "HKT" is that it's an obvious extension of the existing language.
There are lots of useful traits that you can't define if you can't make an associated type generic, so people are excited to have this feature.
It turned out that implementing this in rustc took a very long time, because refactoring the typechecker of rustc to support this was challenging. But that doesn't mean the feature itself is complicated or difficult to use. The whole point of GATs is that it shouldn't really feel like a feature once it's done: naturally, if you are writing a trait with a method that returns `Self::Foo`, but you need it to be `Self::Foo<T>` or `Self::Foo<'a>`, you can just do that, rather than the compiler telling you that it's not supported.
I wish I could become reasonable with rust because I do like the concepts that I think I grasp.
TL;DR: don't worry about it. When you need it, you'll learn it.
What is "not comprehensible for a mortal" in this situation is why adding a generic to an associated type is a long-anticipated feature that took years of development to support. This is related to the "incomprehensible" discussion that occurs around this feature, talking about type functions and higher kindedness and all of these mathematical formalisms. But this discussion is just happening among practitioners and enthusiasts schooled in a certain jargon and area of arcane knowledge. It doesn't mean you need to understand any of this to just use the feature.
It's like people saying the internet is "not comprehensible for a mortal like myself" because most people do not have the background to properly understand IP/TCP/HTTP and how things like this undergirdle the technology that they use. And yet they use the internet just fine. If the design has succeeded, you should not need to even hear words like "higher kindedness" before you can make your associated type generic.
Possibly the system design is imperfect and the abstractions leaks and you as a user have to learn more than one would hope in order to successfully use the tool to accomplish your goals. Rust doesn't have a perfect track record here.
It's frustrating that the documentation / announcement always seems to go for the most complicated way to explain just about everything, in what I assume is an attempt to ensure it's the most comprehensive explanation, in the smallest number of lines. It's fine to provide different levels of explanation/examples.
Numerous times I read stuff and think "Nope. Absolutely no idea why I'd use that, and I'm not entirely sure I have even half a clue what it's trying to do" until much much later when I see more simplistic practical applications of it and comprehension slowly dawns about what was being explained.
trait Foo {
type Bar;
}
Until now, those types had to be concrete, as in the above example. You can implement Foo with `Bar = u32` and `Bar = String` and even `Bar = Vec<bool>`. trait Foo {
type Bar<T>;
}
That `Bar` type above can now be generic over some other type `T` so you can implement it for `Bar<T> = Vec<T>`, `Bar<T> = Result<T>`, `Bar<T> = Option<T>`, and any other generic type. That's it. That's the whole thing. Use it if it's useful to you.If you've never needed to write a trait that incorporated an associated type (.AT) which needed to be generic (G..), then this won't mean much to you and that's fine. But you might be using libraries that could be somewhat more ergonomic if they were able to use this feature. The good news is that now that it's been released to stable, a future version of that library can be written more ergonomically.
People are trying to show how you might use it in practice, which is useful. But I think people should start with the above.
If you’ve ever used associated types, this is the sort of thing you would probably expect would be possible even if you never needed to do it. Now it’s possible.
High-minded or snobbish or whatever other deragotory words that one wants to use: the benefit of something like HKT is that it encompasses one thing that Rust now currently ends up catching up to by implementing dozens of “X with Y” (“generic const in associated lifetimes positions”… to use a made up feature) that are just about lifting limitations, and that end up sounding “complicated” and “featureful” (kitchen sink accusations) to anyone who isn’t knee deep in building an async runtime library or whatever.
Meanwhile a Haskell programmer might go years and never think about HKT as a feature. It’s just “kinds” without artificial-looking limitations.
Fortunately, Niko Matsakis blogged about our design discussions at the time. You can read more here and in the linked predecessor posts (at the time we were calling GATs "ATCs") https://smallcultfollowing.com/babysteps/blog/2016/11/04/ass...
And yet, at the same time, Haskell has many, many extensions to enable certain features that would come for free in a dependently typed language, for example. I find Idris's type system easier to fit in my head, for example.
There's always a next level of generality in which previously complicated concepts can be expressed more simply, but there are also sometimes reasons not to want to reach that level of abstraction, for various reasons.
(Although in principle, I agree. I don't find the concept of HKTs particularly complicated per se.)
For sure.
In Rust’s case though it seemed that insiders were saying that HKT was more than the language needed, while now they keep running into limitations which necessitates patching up feature limitations. And patching up the language itself, not compiler extensions (maybe rustc is the only compiler that people use (?) but the updates are really to the language (in the abstract) itself).
But in any case, I’ve been told that I’m just talking out of my behind ;)
Suppose you have a concrete type like
Vec<i32>
Regular generics enable you to make the contained type generic, so you have: Vec<T>
where T can be i32, u32, String, etc. GATs allow you to make the container generic. So you can have: T<i32>
where T might be Vec, Option, Box, etc.You could be 100% right (I have no idea, but it sounds reasonable), but this entire question really hits home that Rust has tons of basically incomprehensible design features.
The worst part about features like this are that a few smart people will actually use them (whether because they're powerful features, or just because they feel powerful when using them), and then render their code incomprehensible to the mere mortals who have to support their code in the future.
The KISS principle (https://en.wikipedia.org//wiki/KISS_principle) is sometimes ignored because people don't want to be stupid, and why wouldn't you want to use a new toy(tool) once you've figured it out?
Furthermore, the amount of hand-wringing over this feature is disproportionate to its actual impact. It feels like people see "Haskell" mentioned in the same breath and immediately lose all sense of reason. This feature is not basically incomprehensible, it's a straightforward addition that allows generics to be used with associated types, both of which are features that already exist in the language.
If you're asking yourself, "when would I ever need to use these?", this answer is that you may never find call to use these. Traits are mechanisms for creating abstractions, which are mostly relevant to library authors. If you're not a library author, you may never have a good reason to write a trait. Even for people who are writing traits, they may have no reason to ever use associated types. And even for people who are writing traits with associated types, they may have no reason to ever want to use generics with those associated types.
However, all of these features are extremely useful for the people consuming libraries, and far from making Rust code more complicated, from what I've seen so far it makes Rust code in the wild less complicated.
The question is do we inhibit the wise to protect us from the unwise? The answer might be yes. I enjoy learning about power features so I hope not.
I really don't understand what motivates this kind of petty hostility.
It could have been written about any of a thousand language features that didn't exist in assembly and now exist in higher level languages, and it would be just as sad.
Casting to Collection loses the static type info, so you couldn't write foo(bar(set)) if bar returned a Collection but foo accepted a Set.
Not a Rust guy but I think that's the idea.
not only container types, it can be any type that has a consistent context of evaluation for the final value being calculated: "doing async IO", "doing optional presence", "doing potentially erroneous extraction", and so on. In that sense, "doing multiplicity of values" is the context expressing a container type you mentioned as an example.
Rust has made it years without needing this (granted, a few things have been less than ideal because of it) because, for the most part, people have not needed to reach for this type of abstraction. But it's really just "Generics in a new place".
Personally, I've accidentally written the GAT syntax years before I ever heard the term GAT because I thought a generic could go there.
Obviously there are use cases for GAT, what I'm saying is that they're going to be in very generic library code, which I don't think most users are writing.
For libraries like Hyper, which prioritize being extremely generic (it's split into a ton of reusable crates), which aims to serve as a very low level set of generic primitives for HTTP, yes it will come up.
Hyper is a great example of a use case that 99.99% of developers are not going to be able to relate to because they aren't building a set of completely generic http primitives. It's an important use case, it's just not relatable.
Whereas I stumbled upon a use for these ~4 years ago and was just mad that what I naturally wanted to do didn't work =)
But yeah in certain programming domains, probably most, it just doesn't come up.
Associated types are "outputs", they are controlled by the impl (`<Vec<String> as IntoIterator>::into_iter()` will always return a `std::vec::IntoIter<String>`, because that is how the trait is implemented for `Vec<T>`).
You can often use generic parameters for "outputs" as well (you could have a world where you had `impl<T> IntoIterator<IntoIter<T>> for Vec<T>`). However, this prevents the compiler from inferring the "output" type since there is no longer a guarantee that it is unique for a given set of "inputs".
A: type level functions from types to (associated) types (i.e. normal generics)
B: type level functions from types to type level functions (either As or Bs) (GAT).
with generic parameters (i.e. higher kinded types, or template template parameters in C++) you could have type level functions that map type functions to (types or type functions), but once you have GATs you can fake them by passing to first order functions a type that has an associated type.
In practice you could basically just have generic parameters and not bother with associated types, but it would be much less nice to use these APIs.
Here's an example of how an API would change if Rust didn't have associated types:
let x = [1,2,3]; // an array of i32
let mut y = x.iter(); // an iterator over it
y.next(); // pull an item from the iterator
If Rust didn't have associated types, then that last line would have to look like this: y.next::<&i32>(); // if Rust didn't have associated types impl<I, F> LendingIterator for Map<I, F>
where
I: LendingIterator,
F: for<'a> Mapper<'a, <I::Item as GivesItem<'a>>::Item>,
{
type Item = dyn for<'this> GivesItem<
'this,
Item = <F as Mapper<'this, <I::Item as GivesItem<'this>>::Item>>::Output,
>;
fn next(&mut self) -> Option<<Self::Item as GivesItem<'_>>::Item> {
self.iter.next().map(&mut self.mapper)
}
}
Haskell is so much easier.You have `for<'a>`, which I believe is probably the most complicated part of the language. That's a Higher Ranked Trait Bound, https://doc.rust-lang.org/nomicon/hrtb.html
It just means "for all possible lifetimes 'a" as opposed to a single specific instance of a lifetime. Honestly, my brain kinda knows when to use it and I've never bothered to push past that point and into "and I understand why it knows when to use it".
You have nested types, which always makes things a bit messy, but then you have the "I have a type, it implements a trait, and I want to refer to the associated type for that trait's implementation for that type".
ie: <MyType as ATraitItImpls>::Associated
Oof. But once you know what it is it's quite clear and explicit. You wouldn't want to do `MyType::Associated` because what if MyType implements multiple traits that have associated values named Associated? Rust prefers explicit.
I actually use this all the time now because it makes refactoring so easy to only refer to types through generic paths. If I change "Associated" I don't have to update anything at all.
When you nest these it gets extra messy, but it's just the same thing. Mapper's second generic parameter is just the associated type of some other thingy. I wonder if some type Aliasing would help, idk.
Then there's `'_`, which I've never bothered to use because I learned rust before it was a thing and so I just don't really care. The irony is that `'_` exists to make things clearer - it's just an annotation that says "there's a lifetime here that we don't actually have to write down, but I'm writing it so that you know what it is and that it's not some other lifetime".
https://dev-doc.rust-lang.org/beta/edition-guide/rust-2018/o...
So yeah, I get it. Rust is very explicit, and when you have a lot of nested stuff like this that explicitness can look a bit overwhelming. But I'll say that, knowing all of this, I can look at that code and trivially parse it because there's no ambiguity whatsoever.
For me, I am totally OK with remembering and resolving ambiguities in context (both Haskell and Scala are heavy with these). But when everything is explicit and visible all at once, I am stuck, because of information overload (I have ADHD, so there’s that).
Maybe Rust is just not for me.
"I would offer a bit [of a] non-trivial example"
Which is to say, this comment is deliberately trying to go above and beyond the ELI5 request from the OP. You may find the answer below that one a bit easier to understand: https://old.reddit.com/r/rust/comments/ynvm8a/could_someone_...
edit: the "What are GATs" paragraph in the link posted by @leetrout is much more usable.
You have a Rust-like enum that contains data like Option or Maybe, you want to abstract over it. There you have a rudimentary Monad.
What does that mean? Any example you could point to?
Similarly, you have Option::and_then, which takes a callback that takes T and returns an Option<T>. Or Result::and_then.
"Monad" is simply the umbrella term for all flat_maps, and things like it, in existence.
Why would you want to abstract over it? For one, it's a surprisingly common design pattern in programming. You also quickly notice, for example, that filter_map is actually just a special case of flat_map since Option implements Iterator.
You can also study the properties of how such patterns behave. Monads are particularly interesting for a number of reasons. Monads are a kind of "most general" and least constrained form of sequencing, and it turns out that if your large-scale system is monad-like it can have a significant performance impact!
You don't need to have a thing called "Monad" in your type system to do any of the above. Having a thing called "Monad" in your type system does let you write code that abstracts over all flat_maps (and things like it) in existence, which has been used in the Haskell world to build a number of interesting libraries. But "monads" as a concept are useful to everyone in programming.
https://doc.rust-lang.org/std/iter/struct.Map.html#impl-Exac...
However, there is no such implementation for FlatMap<I, F>:
https://doc.rust-lang.org/std/iter/struct.FlatMap.html
This intuitively makes sense! If a single input can generate zero, one or more than one outputs, then there's no way to know the exact size of it.
The simple map is more constrained -- it has to generate exactly one value -- which means that it can support more operations. The monad-like flat_map is less constrained, and it can do more, which means that it can't support as many operations. There's a catchphrase in the programming language community to describe this sort of thing: "constraints liberate, liberties constrain".
This can make a huge difference in production systems. For example, if you have a distributed system that walks over an execution graph, and nodes in the graph can create new nodes (flat_map/monad-like), the properties of your system are very different from if nodes can't do that.