15 ms·
Rust: “Explain GATs Like I'm 5 Years Old”
- aslak 4y agoNo 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).
- yakubin 4y agoPersonally, 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.
- jgilias 4y agoThis 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.
- _448 4y agoOkay, so this is like so in C++: template<template <typename> T> class C{}; Am I right, Rustaceans? :)
- Rusky 4y agoYou 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: class C { template <class> using T = ...; };
- jstx1 4y agoAs 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.
- unsafecast 4y agoBasically, 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.
- wisnoskij 4y agoI really wonder about the usefulness of implementing a language feature to reduce the need to copy paste in an editor.
- mcronce 4y agoGenerics 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.
- wtetzner 4y agoReally? 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?
- cercatrova 4y agoIsn't that basically all language features? I could also write print("Hello") print("Hello2") print("Hello3") print("Hello4") print("Hello5") (which I partially copy-pasted above) but I use a for-loop instead.
- mseepgood 4y agoI seriously doubt a 5 year old can understand this explanation.
- lsllc 4y agoThere's another (better) example down in the comments using a PointerFamily trait; someone asked "ELI4" and the answer is pretty good: https://old.reddit.com/r/rust/comments/ynvm8a/could_someone_explain_the_gats_like_i_was_5/ivcax9t/ https://old.reddit.com/r/rust/comments/ynvm8a/could_someone_...
- royjacobs 4y agoI mean, a GAT is just a monoid in the category of endofunctors, after all.
- kibwen 4y agoGATs don't actually let you go full monad, so not quite. :P
- deleted 4y ago[deleted]
- fleur-de-lotus 4y agoDon't insult me !
- hitpointdrew 4y agoI am over 30 and I don't understand the explanation. Probably because first and foremost I don't even know what a "GAT" or "GATs" are/is.
- kibwen 4y ago"Generic Associated Type". 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.
- leetrout 4y agoGAT is Generic Associated Types https://blog.rust-lang.org/2022/10/28/gats-stabilization.html https://blog.rust-lang.org/2022/10/28/gats-stabilization.htm...
- deleted 4y ago[deleted]
- agentbellnorm 4y agoI’ve seen this concept being talked about for a couple of days now. I even read the Reddit thread this morning. But until now I didn’t know what the letters stand for. Thank you. Death to abbreviations!
- tialaramex 4y agoIt'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.
- stefan_ 4y agoRust better figure out quick how they can shut down the Haskell & C++ language artisans or it will be their downfall (in favor of something like Zig).
- josephcsible 4y agoRust 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.
- bsaul 4y agoIf 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..
- pjmlp 4y agoTypescript. As for Haskell, their motto is "avoid success at any cost" for a reason.
- gpderetta 4y agoC++ sorta has the equivalent of this feature in the form of nested templates. C++ is kind of popular and rust wants to be a direct replacement. Next step: template template parameters!
- kibwen 4y agoThe whole design of GATs is predicated on the notion that Rust doesn't want to go as far as Haskell does here. See boats' comment: https://news.ycombinator.com/item?id=33506540 https://news.ycombinator.com/item?id=33506540
- titzer 4y ago> The stronger your type system is... 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-architecture-astronauts-scare-you/ https://www.joelonsoftware.com/2001/04/21/dont-let-architect...
- karmakaze 4y agoIs 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: 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(?!). [0] https://github.com/rust-lang/rfcs/issues/294 https://github.com/rust-lang/rfcs/issues/294
- celeritascelery 4y agoThese 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. https://cohost.org/oli-obk/post/165584-ranged-integers-via https://cohost.org/oli-obk/post/165584-ranged-integers-via
- karmakaze 4y agoI'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
- celeritascelery 4y agoI first heard about this feature in OCaml. I got the name subtype or pattern type from this thread: https://internals.rust-lang.org/t/thoughts-on-pattern-types-and-subtyping/17675 https://internals.rust-lang.org/t/thoughts-on-pattern-types-...
- the__alchemist 4y agoI'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.
- rufusroflpunch 4y agoSame 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.
- kibwen 4y agoBoth 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.
- secondcoming 4y agoMany C++ developers have walked that path, never to be seen again.
- kitd 4y agoYeah. I'd like to meet the 5-year-old that understood that. edit: the "What are GATs" paragraph in the link posted by @leetrout is much more usable.
- nicoburns 4y agoI think the easiest way to understand it is this: 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.
- aarobot 4y ago
- Georgelemental 4y agoIn my experience, GATs seem like arcane magic until you need them, and then they make perfect sense.
- wisnoskij 4y agoI 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.
- jamincan 4y ago`&'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.
- celeritascelery 4y agoI 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.
- lucasyvas 4y agoThe 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): https://rustwasm.github.io/docs/book/ https://rustwasm.github.io/docs/book/
- leetrout 4y agoThis is exactly why open source communities need more than programmers.
- tracker1 4y agoAs 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.
- stonemetal12 4y ago>Stick to things like apple, orange, pear 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.
- deafpolygon 4y agoI'm not familiar with Rust, but this seems pretty similar to how C# handles generics. Am I misunderstanding?
- Smaug123 4y agoC# 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).
- stephc_int13 4y agoThere 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?
- mrkeen 4y agoBecause there's no correct level of abstraction. There's only appropriate abstractions for different situations.
- jstimpfle 4y agoMaybe there are incorrect situations, though.
- kibwen 4y agoBut 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.
- jstimpfle 4y agoI 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.
- npigrounet 4y ago
- jeremysalwen 4y agoAlthough 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: 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.
- jeremysalwen 4y agoThis 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 ;)
- deleted 4y ago[deleted]
- gpderetta 4y agoLet me try to explain GAT from a C++ programmer perspective: '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
- cryptonector 4y agoThat thread links to https://www.fpcomplete.com/blog/monads-gats-nightly-rust/ https://www.fpcomplete.com/blog/monads-gats-nightly-rust/ which is very nice. Bottom-line: Rust GATs are not enough to get Haskell style monadic programming yet.
- kazinator 4y ago> This functions kind of “unpacks then type”, works with it's internals and then “packs everything correctly”. Right, like I'm five years old and hence don't know what a monad is. :)