The top Reddit example is tremendous. I'm passed being a Rust beginner, but I have a note to the Rust community:
Your official examples are overcomplicated and bad and you should also feel a little bad.
Stick to things like apple, orange, pear and you'll see much easier adoption than if you go with something like LendingIterator! Through the GAT process all I have seen is the same hyper-specific example used that confuses the hell out of people that just need the "Hello World" version.
If you want, I will volunteer to be the test subject "dumb" Rust programmer to vet the quality of future examples.
Tangentially related - here's a WASM example that goes from "Hello World" to Conway's Game of Life (I shit you not, that's actually how the tutorial progresses):
I think this is a failure of messaging rather than a failure of example-writing. Despite all the hullabaloo, GATs are not a "feature", they're just the lifting of an arbitrary restriction that used to exist in the typechecker. There doesn't need to be a section on GATs in any Rust book, for example. It's just generics in associated types. The examples you've seen like LendingIterator are not intended to be pedagogical, they're intended to showcase some specific cool things that people (who are presumed to already know Rust) will now be able to do, and preview things that might someday find their way into the stdlib as a result of this new ability.
For sure, if people want to learn how to use generics in associated types, then there can be examples catering to those users. But I think that framing this as a "feature" makes people feel like they need to go out of their way to "learn" them, when in reality users will either try to use them naturally (as a result of prior knowledge about generics and traits) and it will "just work" and they'll never think twice about it, or they'll never try to use them because they see no need to based on what they're trying to write.
GATs are fundamentally a higher-level abstraction similar to higher-kinded types (you could see them as a very small subset of HKT).
This means:
- they are not trivial no matter how much you bend it
- you should rarely use them in your day to day work (use as in "create traits with GATs")
- they allow you to abstract over certain things in a much nicer way. This thinks matter a lot for certain kinds of libraries. Which is why we do include them even through they are not simple (i.e. once some of the current limitations are lifted I expect there is a high chance for them to be used in the next major version of all of actix-web, axume, tower and a bunch of async libraries).
Through I agree that the example on the blog announcement and on reddit aren't grate. If GATs would just be for the problems described there adding them to the language wouldn't be worth it. (I mean adding a different more complicated and harder to use iterator trait which only matters for a small handful of use-cases really isn't enough reason to add such complicated language feature, on the other hand being able to abstract over async functions properly is a major boon).
Another problem is that some of the "better" examples currently can't be implemented in a straight forward way to the limitations which GATs still have for now.
While I am no pedagogical psychologist, I have witnessed mutiple times, how poor unrealistic examples give people a bad understanding of the thing they are trying to learn. Going with Apple, Orange, Pear would give people an idea of what the keywords did, but no understanding of when it was a good idea to apply the technique.
I think that is one of the primary reasons OO sucks so badly today. Everyone was trained on Fruit, Shapes, and Cars so they know the keywords, but don't have the foggiest clue of when it is a good idea to use them.
Not that I am suggesting LendingIterator! is a super wonderful example either. A good motivating example like 95% of the difficulty, and reward of programming tutorials.
The post you're complaining about starts with "because everyone likes to use that LendingIterator example I would offer a bit non-trivial example." It sounds like they are already doing that.
I think the point is that even for beginners these examples are dumb. If you want to explain OOP and inheritance you can use simple but realistic examples, like `class Shape { fn draw() }`, `class Rectangle extends Shape`, `class Circle extends Shape`, rather than `class Dog extends Animal` or some dumb stuff like that.
As a Rust beginner, have to completely agree... A lot of the examples are overtly complex. Beyond this, sometimes finding appropriate use case examples for libraries harder still. Mostly in that you have to have a very deep knowledge base to even begin in a lot of ways. I mean, I get it, but it's still much harder to get your feet wet beyond "Hello World" type examples in practice.
Note: been a rust beginner for a while, as I play with it, (re)read a couple books I have on Rust, but then set it aside again as I'm unable to justify using it vs. just getting something done. I really like a lot of the concepts but digging in has been repeatedly difficult.
I thought that was pretty much the motivation. HKTs are hard - hard to implement with Rust's low overhead goals, and hard to teach to newbies. My understanding is that GATs came about as some simpler lower hanging fruit to try first.
It's no different in everyday life. This was an appropriate title for the Rust thread, but is too terse for Hacker News.
If I say to my mother "Hannah told me she'd be away for Christmas" we both understand who is meant, my sister. If I say that to my friend Dave it's unclear. Which Hannah? We have a mutual friend called Hannah, I have a sister named Hannah, or maybe I mean the Hannah he works with. So I should be more specific in that context.
Being obliged to be fully specific all the time is no problem for a machine but it's tiresome for humans, so no, I do not agree with "death to abbreviations". Maybe HN rules should encourage people to expand abbreviations when citing material from elsewhere that could be unfamiliar to most readers. As it is, hey, free Internet points for whoever first expands the abbreviation in a comment.
I'm filing this one along with Monads and Haskell as programming concepts that I'll never understand. Rust is my favorite language, but that explanation, lauded directly below it as This is personally the easiest to understand example I've seen of GATs., was incomprehensible.
I hope you don't. To establish my credentials for this comment - GATs were my idea.
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.
Not trying to make you feel like you have failed, but whenever I encounter language features on this level - aka not comprehensible for a mortal like myself - I do tend to want to shamefully turn away from this profession entirely. The good news is that I am most likely a total dunce.
I wish I could become reasonable with rust because I do like the concepts that I think I grasp.
You're begging the question when you say that GATs are "not comprehensible for a mortal like myself." My entire point is that they were designed not to be incomprehensible.
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.
> My entire point is that they were designed not to be incomprehensible.
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.
To be super clear I'm not trying to disparage you at all. I am just saying that I might not be cut out for understanding these complexities and ... honestly it deeply frustrates me, but I don't know how to overcome it.
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.
When I find a language feature I don't understand even after reading what's out there, I ignore it. Then I plod along with my own code. Eventually I'll notice I've been writing the same boilerplate code again and again, and I'll finally see the use for the feature. At that point it's less a matter of "understanding" in a grand intellectual sense, and more just fitting my needs to the syntax. After a few such instances, I know the feature well enough to unblock someone else who doesn't get it. That's a good practical test of understanding.
TL;DR: don't worry about it. When you need it, you'll learn it.
Indeed, when I was very first learning to code I did this with arrays! Couldn’t understand the advantage of writing foo[0] and foo[1] over foo0 and foo1. And in some cases there isn’t really one. But of course arrays are very useful in general.
GATs seem easily understandable to me and I'm a "mere mortal". Like most things in software, the utility becomes clear when you actually have a practical purpose for its use. Don't get ahead of yourself.
> 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.
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.
Before writing a completely asinine comment like this, you might consider that I, having been paid real American dollars to design this language, might know more than you about the relationship between GATs and HKTs and the design of Rust as a whole. Your comment evinces a total ignorance of the type theoretical issues that actually informed our decision on this issue.
> Meanwhile a Haskell programmer might go years and never think about HKT as a feature. It’s just “kinds” without artificial-looking limitations.
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.)
> 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.
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 ;)
Thanks, this was much easier to understand than the original article. What will I use this for? Usually for the “T” to be useful it needs to have some interface. In Java I’d just use Collection<Integer> or something more abstract.
I forget what exactly a GADT is (even though I've used Haskell a bit before). But just going by the linked example from Reddit, you could make a generic `map` function that worked on any container type. You'd have to implement the map function separately for each type. But then you could make a bunch of utility functions that you'd implement once that use the generic `map` under the hood, and all of those container types would be able to use those utility functions.
Just so that we're clear on acronyms for everyone out there, "GADT" is a computer science term for "generalized algebraic data type", which has no relation to "GAT", which is just Rust terminology for generics in associated types (even in Rust, I doubt that anyone will bother to refer to "GAT" in the coming years, rather than just lumping them in with the concept of associated types in general; there's really no need to teach them separately).
> you could make a generic `map` function that worked on any container type.
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.
The part that says `T<i32>`. GATs don't allow you to do that, not directly. GATs allow you, as said by Boats in https://news.ycombinator.com/item?id=33506540, have a generic type as an associated type, nothing less, nothing more. If you want to express `T<i32>`, GATs _enables_ you to do that as a kind of a distorted encoding, but it's not as simple as https://news.ycombinator.com/item?id=33505810 makes it seem.
Bring able to abstract over Rc/RefCell and Arc/Mutex with marker types seems pretty useful. It's not directly HKT but it enables use cases that would otherwise be supported by HKT.
What you're describing sounds to me like higher kinded types, which are of some relationship to GATs (GATs enable a better encoding of higher kinded types as I understand it), but are not GATs. https://docs.rs/higher/latest/higher/
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?
This is quite uncharitable. Rust's features aren't there for no reason, they're there to address the hard problems of how to achieve C++-level performance while providing the level of abstraction that modern developers expect and remaining totally type-safe.
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.
Things like GATs also allow those smart people to add sorely needed features, like async traits, which the rest of us plebeians can benefit greatly from
> 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.
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.
The ability to just use T for Vec, Box, etc, anywhere you might want to is "higher kinded types" - GATs only allow it specifically for "associated types" on a trait.
I think 90% of the reason why people have a hard time with GAT is because there aren't a ton of use cases. From what I can tell, GAT is quite simple - it is just like all other Rust generics except that now the generic can be in a new place.
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.
It's needed for a lot of features people desperately want. Async functions in traits, closures that return futures where you need to run the future in a loop, and array indexing that doesn't necessarily return a reference, are three examples of things that essentially need GATs in order to be implemented without allocating. You may not feel like you missed it but not having that leads to a ton of ugly workarounds in Rust code. Also, in a lot of cases where people would otherwise use GATs, people instead work around them by making traits generic and have super complicated towers of generic types, you've probably seen interfaces like this when using hyper for instance. GATs would simplify most of those interfaces.
The vast majority of users do not desperately want async functions in traits, they just use async_trait and go "it'll be cool when GATs are done". Besides, no one is going to use GAT for async traits, that will be done for them by the compiler.
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.
I know people who refuse to use async_trait for documentation reasons alone. The macro-based solutions aren't always pleasant to use and often come with a lot of weird tradeoffs. And yeah, I agree that most users don't need to write theme explicitly--I'm arguing that users are currently significantly impacted by the lack of them, even if they are never in a position where they'd consider writing them themselves, because it prevents library authors from writing precise APIs. For example, you might not be wanting to write GATs like the Hyper authors, but anyone using Hyper for anything nontrivial (and that's surprisingly common, IME) is certainly negatively impacted by the massive complexity caused by needing to work around their absence.
You mention that you use Rust. In that case, you're almost certainly familiar with generics, and you're likely familiar with associated types. If you've ever found yourself struggling to write an associated type that needed a generic parameter, this feature is for you. And if you haven't ever needed to do such a thing, that's totally fine, and you can happily ignore it and content yourself with benefiting from libraries that are now able to expose simpler and more powerful APIs by leveraging this feature under the hood.
I appreciate the explanation. I've been more of a firmware or application developer in Rust than library developer, so will keep this in the hip pocket.
Yeah - it's almost better to think of GATs as less of a feature and more of a weird restriction being removed. That's obviously super reductive knowing the amount of work that went into it, but from an end developer standpoint, it's an explanation that helped make it make sense to me.
I've yet to figure out why to use associated types instead of generic parameters, and every time I've seen an explanation I nod my head in confusion and carry on.
Associated types serve much the same role as generic parameters in the context of traits, but with the crucial distinction that associated types are specified by the person implementing the trait for their type, whereas generic parameters are specified by the person calling the methods defined by the trait.
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
You can view generic parameters as "inputs", they are controlled by the consumer of the type/trait (you construct a `Vec<String>` because you want to store a bunch of `String` values).
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.
I feel you, this is some particularly rough Rust code, combining quite a lot of stuff that I don't think I directly interacted with for my first few years.
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".
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).
Same here. I presume that one day I will have to write something using a GAT, and then the understanding will click with me, even if I don't speak the terminology.
Both generics and traits are mostly for library authors trying to make expressive APIs. If you're not creating libraries but instead consuming libraries, then it may be natural for you to never need to reach for this feature.
No, monad is more complicated than that. There is a specific method that it has to have, which does not make sense on all types, and can also have more than one sensible definition for a type. "You want to abstract over it" gets you to a "trait", not a "monad".
Are you familiar with flat_map? It's the ability to take, for example, an iterator, then map over each element which itself can return an iterator.
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.
By the way, as a simple example of what I mean when I say that monads are the least constrained form of sequencing: flat_map is strictly more general than map. In Rust, if an iterator I implements ExactSizeIterator, then Map<I, F> implements ExactSizeIterator:
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.
I feel like they are making languages too complicated. Most projects should just be garbage collected, most don't need to deal with pointers or anything advanced. We need variables, functions, conditionals, loops, exception handling, struts, and a library with a bunch of standard utilities (including simple I/O and string managment). Everything else should be optional and kept out of sight unless you are looking for it IMHO.
I am probably just old and set in my ways, but I have been learning rust over the last few days, and it is really syntax dense. Their is no reason it needs to be this complex. For example I think "String" and "&static' str" are different types, and no one explains what "&static'" is but I dont think it is changeable. It is not like ' or static means anything and can change, it is just basically random syntax you have to memorize. OK, I am sure it means something like a reference to a static character or something, if you dont even think it is a good idea to inform people what it means, dont implement the syntax like that.
I will agree with you that most languages should just be gc. Rust is niche; targeted at applications where you need control over memory and layout. It is not a good general purpose language.
That being said, most of things you pointed out as “no reason to be this complex” do have good reasons. String and &str could be renamed to StringBuf and StringSlice. The String type lets you manipulate the string value, but it is a bigger type than a slice (&str). &’static just indicates that the string slice will live for the entire program. As you continue to learn Rust, more of these distinctions will make sense to you.
GAT’s themselves are actually a great example of a feature that adds capabilities without really adding complexity. They feel like they should have always been there, and I feel like anyone who has worked with the language for a while has implicitly tried to do this and was surprised it didn’t work.
> I feel like anyone who has worked with the language for a while has implicitly tried to do this and was surprised it didn’t work.
Just to lend some extra evidence to your assertion, I hit the lack of GATs within two days of starting to learn Rust. In the natural course of trying to solve a problem, I tried to write `LendingInterator`, and discovered that I could not.
Agreed, people should not assume that the design of Rust is some sort of statement about how higher-level languages should operate. It's a very targeted design with specific goals. For instance, the fact that Rust eschews exceptions should not be interpreted as "exceptions are bad", but rather "we don't think exceptions are a fit for what we're trying to achieve in this one specific domain".
certainly complex from a implementation standpoint, and some of the rough edges can be a footgun around lifetimes until those are ironed out. But being able to add generic arguments to associated types is just a natural extension of the language from a user standpoint. You could already add generic arguments to functions, structs, type aliases, enums, etc. This just allows it in one more place.
Most things don’t need to be written in Rust. But C/C++ exist for a reason, and Rust for effectively the same reasons.
Rust looks complicated but it’s a complicated problem that’s why the solution is complicated.
If you want zero cost (e.g no GC!) abstractions and some compile time guarantees about concurrency and memory safety well then you have a complex problem requiring a complex solution.
&str is a “slice” of a string, it doesn’t own any characters it just points to someone else’s characters. String is a heap allocated string so it has its own characters.
The ‘ indicates a lifetime and “static” just means “lives forever”.
In the end, lifetimes and multiple string types is the price we pay for performance and safety. There is no world where Rust just had ergonomic strings like Java and safety/performance unaffected.
`&'static str` is an `&str` (string slice) with a `'static` lifetime (ie. lasting the duration of the program). It is most commonly encountered with string literals.
In some languages, it is appropriate to gloss over the differences between `String`, `&str`, and `&'static str`, but in Rust, all of those details are meaningful and important. If distinguishing between them isn't important to you, there are other languages that probably better reflect your needs.
> I feel like they are making languages too complicated.
Perhaps. But this topic is not a natural springboard for that topic since GATs are about lifting a current restriction in Rust. So you have the same features as yesterday, but with one less (arbitrary-looking) restriction.
Maybe it makes the language more complicated to implement (?) but it doesn’t make it more complicated for the language user.
Ah, so this probably tells the compiler where to insert the free? If I gave it a local lifetime it would free the memory on function return for example, even if I tried to return a reference to it? Basically Rust just forces you to code in the memory freeing at the definition state of variable creation?
Not quite. Lifetime annotations prevent you from accidentally using references after the value they refer to has been freed. They track how long things will live, instead of defining how long they will live.
Basically they turn use-after-free errors into compile errors.
(I'm using 'free' here to mean cleaned up in general. Lifetimes can track stack values.)
It looks like I will just have to bite the bullet and actually read the rust book back to front. I find static particularly confusing because of the seemingly contradictory meanings that are all jumbled up in my head. The English meaning of immutable, the Java meaning of instance agnostic, and this Rust meaning of immortal lifetime.
> It looks like I will just have to bite the bullet and actually read the rust book back to front.
I mean, there you have it, right? Not a diss on you specifically, but I've often noticed that some people will complain about something, but then it comes out that they actually haven't learned it deeply enough, they're just going on what they either learned so far, learned that something in a haphazard way, or didn't learn it at all.
I used to do the same, so no shame, starting off writing programs as practice and googling things along the way because everyone on any programming thread says "the best way to learn is by building, not doing tutorials" but that's not necessarily true. Sure, I have learned a lot by building, but I've often spent longer simply running around googling and learning bits and pieces than the time I'd spend actually learning something top to bottom, front to back, from scratch. So I actually like and see the value of tutorials now.
Once I read The Rust Book front to back like you said, I understood a lot more of the language than my previous attempts to learn it a few years ago. Doing Rustlings at the same time was great too.
Garbage Collection is extremely complicated. It is a whole other program running to manage your memory using extremely complex, hyper optimized algorithms that have lots of best and worst case scenarios depending on which one you use.
GC implementations can very easily make code confusing. Consider `finalize` in Java - a destructor that gets called at a completely indeterminate period of time, making it a very confusing tool for resource management.
I find Rust trivial by comparison. But isn't that interesting, how we all view simplicity so differently?
I did want to say Rust was complicated, I meant to say it was complicated to learn. You never need to learn the Java syntax finalize, it is invisible until you have a problem where that is the solution. Similar to Java templates or overloading they dont exist until they are the solution to a problem you are having.
My critique is that Rust appears (and I am probably wrong) to have a problem where syntax for complicated language features appear in the simplest code that even a day 1 grade 8 student needs to memorize, even if they wont understand it until the second year of university.
> syntax for complicated language features appear in the simplest code
IDK, I hear you but I'm of two minds.
1. I think a lot of this is familiarity.
`public static void main(string[] args)`
That's a lot of stuff that I don't actually really need to care about but is in every single Java program. It even has 'static', which is honestly something I found very very confusing when I first learned Java (technically my first language since I took a course at a local community college).
Compare that to c,
`int main()`
That's a lot less in my face for sure.
But if I were used to Python? I'd just write my code directly in the file and it would execute from top to bottom. WTF is this `int` and `main` ???
In Java you have int and Integer, and Integer can be this "null" thing? I just wanted to add 2 and 2 wtf??
Generics? `class Foo<T>` ? WTF is `<>`?
The point is that what you have to learn when you first use the language is really going to depend on what you learned before it. I don't consider that complexity, it's just new.
The thing is that there is no language that you're likely to have used before that will prepare you for some bits of Rust.
2. Rust could be easier. It would be cool if there were a way for beginners to think less about the differences between String and &'a str and &'static str. At the same time, abstracting over those would have its own downsides - if you learn about strings in Rust in a way that abstracts that all out, will you understand the underlying components and how the abstraction works? It's tough.
I think the reality is that Rust is just not going to be as easy to learn as the same exact language but where lifetimes are managed at runtime. It probably shouldn't even try too hard to be that easy, because being that easy has costs, and there's a limit to how much you should optimize for newcomers. Rust has balanced things pretty well so far there - 2018 brought a lot of ergonomics wins that I frankly don't care for very much or even just forget about, but it helped tons of people pick the language up.
All this is to say, I think there's truth to what you're saying but I also think you may be attributing some issues to complexity where I believe it's an issue of familiarity.
I appreciate your opinion on this though, I do always find it so interesting to hear about how others view complexity and programming languages (when they're constructive about it).
Highly agree with #1. I think it really does come down to familiarity. I've learned all the languages you listed and every time there was something that was confusing only to then be cleared up the more I use it.
As a beginner (having finished the book and not much more) the example in the top comment is very difficult to parse and hold in my head. At a glance I have no idea what we're doing and why.
More concretely, it's a pain to have to implement the same functions over and over for slightly different types, even when you're basically doing the same thing every time. Specifically, `Option` and `Result`, for example, both have `map` implemented for them (that's one lot of duplication), but also the consumers of the data structures need to know whether they've got an `Option` or a `Result` (and that's another lot of duplication if you want to allow consumers to use either an `Option` or a `Result` depending on what happens to be locally convenient to them). Sometimes you really do want to do different things depending on whether you've got an `Option` or a `Result`, but much of the time you don't care; you're only using it to signal errors, and you have no particular opinion on how you do so.
The example in the top comment allows you to implement what it means for a type to "have a `map` function". So it doesn't get rid of the duplicated effort on the "supply side" - we still have to implement the `Mappable` trait for each of `Result` and `Option` individually - but it allows the consumer to not care whether it's got an `Option` or a `Result`, by using only the `Mappable` trait instead. That is newly possible because the GAT feature allows the definition of a `Mappable` trait which contains fresh generics, and `map` is inherently a generic function.
Result and Option both implement Try, so if you actually just want to know whether to keep going or give up, the Try trait will give you a ControlFlow which says exactly that regardless of whether your input is an Option or a Result
Basically, before this you couldn't have generic types in traits, like this:
trait X {
type Y<Z>;
}
You couldn't express "a thing that contains another thing" in one of those type expressions before, and also use the contained type. It's needed for Mappable, because it's an abstraction over the concept of modifying a value inside a container.
IMO the implementation that they have is a bit obtuse, but I don't know if you can fit something better in this language.
Generics are a language feature to reduce the need to copy and paste in an editor. For that matter, functions are a language feature to reduce the need to copy and paste in an editor.
Really? You don't see the problem with trying to maintain code that's been copy-pasted everywhere with slight differences? Doesn't it seem easier to maintain one implementation of something than N implementations?
It is totally ok to not understand stuff, and it's totally ok to not want to learn it as long as you don't need it.
Rust is a language with plenty of things to learn, some of which take some time to get used to. And often, having a concrete need for something makes it easier to learn the associated technique.
As a non-beginner, I feel the same way. I'm not too worried about it at the moment, but it's early in the day (and week) and DST happened yesterday, sooo...
Is there any way to use GATs to achieve a particular use case of ah-hoc/anonymous enums[0]? Say if library L1 returns enum A|B, L2 operates on enum A|B, and main wants to pass enum A|B from L1 to L2, with the kicker being A, B are concrete in main.
I'm not interested in distinguishing between multiple appearances of the same type as in the RFC example, so rather like:
The most disappointing thing I've encountered with Rust is not seeing anonymous enums or structural typing other than for tuples. I read somewhere that structural typing is only for tuples because the word struct is in structural(?!).
These are called sub-types or pattern types, and they are not in the language. But they are something I would love to have. You can’t emulate them with GAT unfortunately.
I've not heard this called sub- or pattern types before. Perhaps the rfc syntax is complicating the discussion.
We have struct and tuple in Rust where tuple is an ad-hoc/anonymous struct where only the number and order of member types matter. We have enum which are nominal types. We don't have the ad-hoc/anonymous version of them where only the set of possible types matter.
In TypeScript, F#/OCaml they would be like x: TypeA|TypeB
Thanks for the reference. I can see how values of type A could be considered as a subset of values of type A|B, but I think the 'any value of type A' subset of type A|B is more related to types and less about restricting the range/set of values for a type.
Yes, thank you that should work since both libs L1, L2 and main can all refer to Either<A, B>.
This parallels a similar solution to the nominal sum types I ended up making for Java (Either<A, B>, Option3<A, B, C>, etc) with these generic types in a common shared library and other libraries and consumers of both accessing the shared type.
One more question: In Rust is Either<A, B> type-erased, specializations generated, or something else?
This comment here serves as a gravestone for a long form, well written (I think) explanation of GATs that I thought was actually ELI5 (unlike every other ELI5 response which always immediately starts dropping tons of jargon), which Firefox lost when somehow it randomly activated the back button ;_;.
I could potentially rewrite it if anyone was interested, but I am under no illusions that's likely ;)
You can do similar things with it, but GATs can't live on their own like that. Instead they are always a member of some trait, so a closer comparison would be with something like this:
Although the "LendingIterator" example is apparently done to death, I'm going to try to explain it in a way that is actually appropriate for "ELI5".
In Rust, there are many different ways to have a list of things that come one after another. But sometimes we want to write some code that doesn't care about the details like whether the list is a list of words, or a list of numbers, it just cares about the fact that there are a list of things in order.
Right now, we call these general lists of things in order "Iterators", with code like this:
This tells us what an "Iterator" means. "Item" is the type of thing that is in the list, for example it could be "number" or "word". "next" is a function, which means it is something we can do. When we do the "next" function, we get the next thing in the list. So if we keep doing the "next" function over and over, we can get everything in the Iterator one by one.
However, sometimes we want to have more complicated lists, where the "next" function doesn't actually get us the next thing in the list, it just tells us the address of the next thing inside the computer. We would write this code like this:
However, this requires the thing in the list to always be at the address (that's what "&'static" means). But a lot of the time the thing stays at the address while we are looking at the list, and later it will go away.
We could write code saying that the things only have to be at the address for a certain period of time. It would look like this:
This solves that problem, because now we are saying the address only has to have the thing at it for the the time period "a" (we call "a" the lifetime, because it tells us how long the things will live at the address). The problem is, that when we write "Iterator<'a>" we have to decide up front what the lifetime will be. So we can't use the same code for lists with different lifetimes, we would need to write the same code twice, once for each lifetime.
This problem is what GATs help us with. GATs are a way to say that we are going to have a list of addresses, where the lifetimes could be different for each list. We would write the code like this:
trait LendingIterator {
type Item<'a> where Self: 'a;
fn next(&mut self) -> Self::Item<'_>;
}
This allows us to write code which doesn't decide up front what the lifetime of the addresses will be. Instead, the same code can happily work with lists of addresses with any lifetime. GATs may seem scary at first, but as you can see, they are not doing anything fancy, just allowing us to do what we should be able to do: write one piece of code that can work with lists of addresses with different lifetimes.
C# does already allow you to create interfaces whose methods are generic, yes. The way they are implemented is entirely different: Rust monomorphises all generically-typed functions (I believe this remains true for GATs), whereas the CLR monomorphises only struct-generics (and it uses the JIT to do so, unless you're using the fairly new AOT compilation).
There is a simple but fundamental observation that many experienced software engineers discover sooner or later: Reading/understanding code is much more difficult than writing it.
Because of that, programming languages should be designed to make the reading part as easy as possible.
What is the point of those high-level abstract features if they make reading/understanding the code too difficult?
But this feature makes reading code easier? It allows library authors to absorb the complexity into themselves, allowing them to present easier and more convenient APIs for the users of those libraries. Because programs that consume libraries outnumber the libraries themselves, that results in a net reduction in complexity.
I disagree that this makes reading code easier. It is tremendously more complex to understand a generic API precisely because it is generic - it must cater to a huge variety of use cases, and consequently has more nuts and bolts, and knobs to turn.
Now this can be the right choice in many situations, when the higher cost in understanding the API is amortized over a larger number of uses. But in general I much prefer simple and specialized APIs and don't see much of a need for genericity. This includes error handling and resource allocation.
But using inheritance for this is not great, the result of pair_vector<int> is really not a vector<pair<int,int> >. You can use an associated type:
template<class T>
struct pair_vector { using result = vector<pair<T, T>>; };
now 'pair_vector<int>::result' is exactly 'vector<pair<T,T> >' [1].
So these are first order type functions. Let's say you want higher order type functions, i.e. functions that take other type functions as parameters or return them.
For example let's say you want a to build a pair<X,Y> piece wise: you want a function pair_1st that given a type X returns another function that takes a type Y and finally returns a pair<X,Y>:
template<class X>
struct pair_1st {
template<class Y>
struct apply { using result = pair<X, Y>; };
};
So: 'pair_1st<int>::apply<double>::result' is 'pair<int, double>';
'pair_1st' is an higher order function from type => (to a function form type => type).
Apply here is an associated type, (like result before), but is generic, hence an associated generic type.
So, what if we also want to abstract over pair in 'pair_1st', i.e. an higher order function that takes two parameters, the first a function from type => to type, and the second a type:
template<template<class T1,class T2> F, class X> struct bind_1st {
template<class Y>
struct apply { using result = F<X, Y>; }
};
Now 'bind_1st<pair, int>::apply<double>::result' is again pair<int, double>;
I think that rust doesn't allow higher order (i.e. template template) parameters, but that's not a big problem:
template<template<class F, class X> struct bind_1st {
template<class Y>
struct apply { using result = F::apply<X, Y>::result; } [2]
};
struct pair_fn {
template<class X, class Y> struct apply { using result = pair<X, Y>; };
};
By lifting pair into into a pair_fn class with a GAT we can solve the issue: bind_1st<pair_fn, int>::apply<double>::result is again the same as pair<int, double>. In fact in C++ for a long time, before variadic templates, even if template template parameters were available from the beginning, GATs were the preferred way to encode higher order type functions.
The big difference between rust and C++ of course is that the type level functions in C++ are untyped other than maybe being able to specify the arity, while rust has a proper type system for generics, which I assume make all of this more complicated to implement from a compiler point of view.
Now why would you want all this nonsense outside of hardcore type level programming? Well, there is a continuum from simply writing a generic class to actual metaprogramming, so even if you do not want to do the latter, you might end up using on some of this stuff even for relatively simple generics.
[1] we can use template using to hide ::result, but that's a c++ quirk which is not important in the grand scheme of things.
It's arguably in the category of "pet peeve" more than anything else, but I really hate the ELI5 meme and suspect most people asking for it and/or trying to provide explanations that fit it have never had a five-year-old. Unless your target five year old is a five year old Terence Tao, and honestly even then, most of the time these are terrible.
ELI12 would make a lot more sense; old enough to have had enough life experience to hang some of these explanations off of, young enough to need simple explanations.
Explained in other terms, a generic is just a parameter for types, analogous to how `x` is a value parameter for the function call `foo(x)`.
Meanwhile, an associated type is just a type alias that's associated with a trait (a trait is a class, but with no state), similar to how traits can have associated functions (which are also known as "methods").
GATs, then, "just" let you use generics in associated types, which was previously unsupported in Rust.
GATs are an essential part of Rust's long-term async/await story. It's not language-wankery, this is driven by actual real-world use cases. Rust's own stdlib would have benefited from this in many places had this feature existed years ago.
No, they're not. GATs are used internally in the compiler to implement async methods, but there is absolutely no need for them to be in the language for that (just like generators are used to implement await but are not part of the (stable) language). GATs may or may not be useful, but IMO the actual real-world use cases are pretty weak.
Yes, I've read the stabilization thread and I'm aware of your stance. :P Whether or not it's exposed, the mechanism still needs to exist in the compiler, and enough library authors are clamoring to use it that I think the argument in favor of exposing them is stronger than you're giving it credit for.
You mention generators, but a lot of people are clamoring for those as well, and I fully expect them to be exposed to end-users someday.
My point is that there or may or may not be reasonable justification for adding GATs, reasonable people can disagree on that. But async methods are not part of that justification. Similarly, generators may or may not be good for the language (IMO they would be good to add, if they can be made to work etc.) but they didn't need to be part of the language to add await.
Even if the use cases outside the standard library were weak, given that the work needs to be done anyway for the standard library, why wouldn't you expose them?
To be clear, there are not use cases in the std lib. The use case is as an intermediate representation for the compiler, and that is no motivation for inclusion in the language (in the same way that we don't have vtables or register allocation in the surface syntax)
Being able to have a generic "Mappable" trait seems like a good use case for the standard library. It's something I've wanted in the past, and was disappointed that it didn't exist.
- allows you to write these things with less effort
- is more expressive
I keep an eye on Nim as well. Their syntax for algebraic data types could use some syntactic sugar, but it looks like a powerful and fast language, and the syntax allows you to do away with all the c-style braces, curly braces and semicolons that pesters so many languages and gives me RSI.
More upsides I found: macros on the AST-level and fast compilation; it is a language that deserves more attention.
Well, it's not academic, it solves real problems. More so for library developers than for application developers, but a thriving and great ecosystem of libraries is also good for application developers.
Rust getting more of Haskell's type system features is a good thing. The stronger your type system is, the more mistakes you can discover as compiler errors instead of runtime bugs.
I used to think a stronger and stronger type system was a universal good, then I got experience with code written by architecture astronauts[1] armed with such type systems. Knowing exactly which kinds of things to make impossible at compile time and which not is absolutely an art form. Unfortunately, such type systems seem to result in so many nightmares.
The linked article talks about abstraction, and that you could abstract so much that the concrete problem doesn't fit in your abstraction. I.e., your abstraction is broken.
I think that is precisely why OOP started to disappoint (tbh: I also think many people did not understand OOP really well).
Haskell pushes you to compose types instead of building inheritance hierarchies. Multiple inheritance turned out to be a bad idea too, so yeah, OOP program do typically not have the best models of the problem domain they were intended to model.
At the other end of the spectrum you see people just give up: everything is a kind of dict or a string, or wo knows, null? Looking at you php, python, javascript..
All the improvements you see in todays OOP languages are ideas stolen from functional languages like Haskell. What Haskell could have done better is be strict instead of lazy by default.
To model the real world and make assumptions explicit, a type system, as Haskell gives you, is so nice.
I agree that you can make types also really abstract like some people indeed do. It's fine if you do that as a library author, but you should offer a facade with some simpler type aliases. If you stick to Haskell98 and possibly enable GADT support you already get an immensely powerful language.
Also, I have not used Haskell for a while, but I heard the compiler error messages have become way more human friendly these days, so that helps with complex types.
As with anything, it all goes well until it doesn't. When you have a single bug (e.g. type error or dynamic error), the guts of whatever is in the box spill out. Case in point: all the implicit template parameters to std:: in C++ these days. The only way to add configuration options in a backwards-compatible way was to add default values for template arguments. That's all fine and good when you can't see them, but as soon as you have a nasty type error, the types that come spilling out in error messages have zillions of default arguments that get printed.
It's almost always the same with inference. I would be very, very wary of doing so much type sophistry that it requires heavy inference to be usable.
at the end of the day architecture astronauts delegate pipe laying to plumbers, and the plumbers benefit from existing build plans created by architects with all their sophisticated tooling.
If a typesystem has any impact on the popularity of a language, then i assume rust doesn’t want to copy too much of haskell and needs to know where to stop..
There will always be more things that someone will want to formally verify that don't exist in the type system. How do you decide what is and isn't worth putting into the type system?
No doubt they'll needlessly spread to code that doesn't require them - happened already with async and that is much, much simpler concept (than one of the most complex things, GATs).
Personally, I consider GATs a lot easier to understand than async. It doesn't transform my code into something completely different than I'm seeing. It's just the ability to put a generic parameter in one more place, paradoxically reducing the number of traits.
It's something that you naturally try to write when you don't know it's not supported, rather than an additional thing to learn.
This is different though. Async colors functions, so once you’ve got something that has an async interface, you either go async too, or have to do shenigans to use it in a blocking way. GATs will just let library authors make more flexible interfaces.
If there’s anything to worry about then that may be compile times of complex codebases. I haven’t measured anything but just as a rule of thumb more generic code means longer compile times in Rust-land.
Your official examples are overcomplicated and bad and you should also feel a little bad.
Stick to things like apple, orange, pear and you'll see much easier adoption than if you go with something like LendingIterator! Through the GAT process all I have seen is the same hyper-specific example used that confuses the hell out of people that just need the "Hello World" version.
If you want, I will volunteer to be the test subject "dumb" Rust programmer to vet the quality of future examples.
Tangentially related - here's a WASM example that goes from "Hello World" to Conway's Game of Life (I shit you not, that's actually how the tutorial progresses):
https://rustwasm.github.io/docs/book/