Rust: “Explain GATs Like I'm 5 Years Old”
old.reddit.com
old.reddit.com
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):
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.
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.
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.
Some concepts aren't for beginners. This includes most things that have to do with more advanced type theory or for example category theory.
ELI5 GATs? ELI5 Monads? If we could we would.
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.
https://blog.rust-lang.org/2022/10/28/gats-stabilization.htm...
But until now I didn’t know what the letters stand for. Thank you.
Death to abbreviations!
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.
edit: the "What are GATs" paragraph in the link posted by @leetrout is much more usable.
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.
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 ;)
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.
"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_...
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.
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.
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.
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.
Pretty sure many relevant contributors have said it is the most complex language feature since async ..
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.)
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.
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.
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?
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.
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).
One is supposed to use try-with-resources, determinist resource management, or cleaner types.
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.
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.
print("Hello")
print("Hello2")
print("Hello3")
print("Hello4")
print("Hello5")
(which I partially copy-pasted above) but I use a for-loop instead.As a library user, you can't copy-paste a line for a type you just created in a library you don't maintain.
Being able to do what you describe lets a maintainer create an interface for all the future types that satisfy given requirements.
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.
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.
I'm not interested in distinguishing between multiple appearances of the same type as in the RFC example, so rather like:
let foo: (~str|int) = (_|666);
match foo {
(s: str|_) => println(fmt!("string: %?", s)),
(_|n: int) => println(fmt!("int: %?", n))
}
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(?!).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
https://internals.rust-lang.org/t/thoughts-on-pattern-types-...
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?
I could potentially rewrite it if anyone was interested, but I am under no illusions that's likely ;)
template<template <typename> T> class C{};
Am I right, Rustaceans? :) class C {
template <class>
using T = ...;
};Right, like I'm five years old and hence don't know what a monad is. :)
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:
trait Iterator {
type Item;
fn next(&mut self) -> Self::Item;
}
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:
trait StaticIterator {
type Item;
fn next(&mut self) -> &'static Self::Item;
}
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:
trait ReferenceIterator<'a> {
type Item;
fn next(&mut self) -> &'a Self::Item;
}
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.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?
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.
'int' is a type. 'vector<int>' is also a type
but 'vector' itself can be understood as a function from type to type (for example, invoking vector with an int produces vector<int>)
Now let's say you want a function that given a type produces a vector of pairs of that type. In C++ you could do:
template<class T>
struct pair_vector : vector<pair<T, T> > {};
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.
[2] template typename noise removed for sanity
https://old.reddit.com/r/rust/comments/ynvm8a/could_someone_...
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.
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.
As for Haskell, their motto is "avoid success at any cost" for a reason.
Next step: template template parameters!
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.
https://www.joelonsoftware.com/2001/04/21/dont-let-architect...
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.
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.
Architecture astronauts are going to be a problem regardless of language features.
- 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.
Zig still has to win the love of the giants that ship C++, and increasingly Rust, in their products.
Maybe I should delve again into GADTs in OCaml, before having a second look.
GATs are generic type aliases which are defined inside traits.
GADTs are generic sum types with extra constraints on which variants may be used with which type arguments.
There is no reasonable way in which they are "quite similar," nor do either of them have anything at all to do with affine types.
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.
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.
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.