Rust's Generic Associated Types: What is It?
A Bit of Insight Into Generic Associated Types (GATs) That name is so long! What the heck is this? Don’t worry, let’s break it down from the beginning. Let’s start by reviewing some of Rust’s syntax structure. What makes up a Rust program? The answer: items. Every Rust program is composed of individual items. For example, if you define a struct in main.rs, then add an impl block with two methods, and finally write a main function - these are three items within the module item of your crate. Now that we’ve covered items, let’s talk about associated items. Associated items aren’t standalone items! The key lies in the word “associated” - associated with what? It means “associated with a certain type,” and this association allows you to use a special keyword called Self. This keyword refers to the type you’re associating with. Associated items can be defined in two places: within the curly braces of a trait definition, or within an implementation block. There are three kinds of associated items: associated constants, associated functions, and associated types (aliases). They correspond directly to the three item types in general Rust: constants, functions, and types (aliases). Let’s look at an example! #![feature(generic_associated_types)] #![allow(incomplete_features)] const A: usize = 42; fn b() {} type C = Vec; trait X { const D: usize; fn e(); type F; // ← This is the new part! Previously, you couldn’t write here. } struct S; impl X for S { const D: usize = 42; fn e() {} type F = Vec; } So what’s the use of this? It’s quite useful, but only in specific situations. In the Rust community, there are two classic use cases for Generic Associated Types. Let’s try introducing them. But before diving in, let’s quickly review generics. The word “generic” means “general-purpose” in English. So what is a generic type? Simply put, it’s a type that’s missing some parameters - parameters that are filled in by the user. A quick note: previous translators chose to render "generic" as “泛型” (which literally means “general type”) because many systems let you parameterize over types. But in Rust, generics aren’t limited to types - there are actually three kinds of generic parameters: types, lifetimes, and constants. Alright, here’s a concrete example of a generic type: Rc. This is a generic type with one parameter. Note that Rc by itself isn’t a type - only when you supply a type argument (like bool in Rc) do you get an actual type. Now imagine you’re writing a data structure that needs to share data internally, but you don’t know in advance whether the user wants to use Rc or Arc. What do you do? The simplest way is to write the code twice. It’s a bit clumsy, yes, but it works. As a side note, the crates im and im-rc are largely identical except that one uses Arc and the other uses Rc. In fact, GATs are perfect for solving this problem. Let’s look at the first classic use case for Generic Associated Types: type families. Task #1: Using GATs to Support Type Families Alright, let’s build a “selector” that lets the compiler determine whether to use Rc or Arc. The code looks like this: trait PointerFamily { type PointerType; } struct RcPointer; impl PointerFamily for RcPointer { type PointerType = Rc; } struct ArcPointer; impl PointerFamily for ArcPointer { type PointerType = Arc; } Pretty simple, right? With this, you’ve defined two “selector” types, which can be used to indicate whether you want to use Rc or Arc. Let’s see how this works in practice: struct MyDataStructure { data: PointerSel::PointerType } With this setup, your generic parameter can be either RcPointer or ArcPointer, and that determines the actual representation of your data. Thanks to this, the two previously mentioned crates could be merged into a single one. Task #2: Using GATs to Implement a Streaming Iterator Here’s another issue - this one is kind of specific to Rust. In other languages, either this problem doesn’t exist or they’ve simply given up on solving it (ahem). The problem is this: you want to represent dependency relationships in your API - between input values, or between inputs and outputs. These dependencies aren’t always easy to express. What’s Rust’s solution? That little lifetime marker '_ - we’ve all seen it. It’s used to express these dependencies at the API level. Let’s see this in action. Everyone’s probably familiar with the Iterator trait from the standard library. It looks like this: pub trait Iterator { type Item; fn next(&'_ mut self) -> Option; // ... } Looks great, but there’s a tiny issue. The Item type has no dependency at all on the type of the Iterator itself (Self). Why is that? Because calling next creates a temporary lifetime scope (the '_'), which is a generic parameter on the next function. Meanwhile, Item is a standalone associated type - there's no way to tie it

A Bit of Insight Into Generic Associated Types (GATs)
That name is so long! What the heck is this?
Don’t worry, let’s break it down from the beginning. Let’s start by reviewing some of Rust’s syntax structure. What makes up a Rust program? The answer: items.
Every Rust program is composed of individual items. For example, if you define a struct in main.rs
, then add an impl
block with two methods, and finally write a main
function - these are three items within the module item of your crate.
Now that we’ve covered items, let’s talk about associated items. Associated items aren’t standalone items! The key lies in the word “associated” - associated with what? It means “associated with a certain type,” and this association allows you to use a special keyword called Self
. This keyword refers to the type you’re associating with.
Associated items can be defined in two places: within the curly braces of a trait definition, or within an implementation block.
There are three kinds of associated items: associated constants, associated functions, and associated types (aliases). They correspond directly to the three item types in general Rust: constants, functions, and types (aliases).
Let’s look at an example!
#![feature(generic_associated_types)]
#![allow(incomplete_features)]
const A: usize = 42;
fn b<T>() {}
type C<T> = Vec<T>;
trait X {
const D: usize;
fn e<T>();
type F<T>; // ← This is the new part! Previously, you couldn’t write here.
}
struct S;
impl X for S {
const D: usize = 42;
fn e<T>() {}
type F<T> = Vec<T>;
}
So what’s the use of this?
It’s quite useful, but only in specific situations. In the Rust community, there are two classic use cases for Generic Associated Types. Let’s try introducing them.
But before diving in, let’s quickly review generics. The word “generic” means “general-purpose” in English. So what is a generic type? Simply put, it’s a type that’s missing some parameters - parameters that are filled in by the user.
A quick note: previous translators chose to render "generic" as “泛型” (which literally means “general type”) because many systems let you parameterize over types. But in Rust, generics aren’t limited to types - there are actually three kinds of generic parameters: types, lifetimes, and constants.
Alright, here’s a concrete example of a generic type: Rc
. This is a generic type with one parameter. Note that Rc
by itself isn’t a type - only when you supply a type argument (like bool
in Rc
) do you get an actual type.
Now imagine you’re writing a data structure that needs to share data internally, but you don’t know in advance whether the user wants to use Rc
or Arc
. What do you do? The simplest way is to write the code twice. It’s a bit clumsy, yes, but it works. As a side note, the crates im
and im-rc
are largely identical except that one uses Arc
and the other uses Rc
.
In fact, GATs are perfect for solving this problem. Let’s look at the first classic use case for Generic Associated Types: type families.
Task #1: Using GATs to Support Type Families
Alright, let’s build a “selector” that lets the compiler determine whether to use Rc
or Arc
. The code looks like this:
trait PointerFamily {
type PointerType<T>;
}
struct RcPointer;
impl PointerFamily for RcPointer {
type PointerType<T> = Rc<T>;
}
struct ArcPointer;
impl PointerFamily for ArcPointer {
type PointerType<T> = Arc<T>;
}
Pretty simple, right? With this, you’ve defined two “selector” types, which can be used to indicate whether you want to use Rc
or Arc
. Let’s see how this works in practice:
struct MyDataStructure<T, PointerSel: PointerFamily> {
data: PointerSel::PointerType<T>
}
With this setup, your generic parameter can be either RcPointer
or ArcPointer
, and that determines the actual representation of your data. Thanks to this, the two previously mentioned crates could be merged into a single one.
Task #2: Using GATs to Implement a Streaming Iterator
Here’s another issue - this one is kind of specific to Rust. In other languages, either this problem doesn’t exist or they’ve simply given up on solving it (ahem).
The problem is this: you want to represent dependency relationships in your API - between input values, or between inputs and outputs. These dependencies aren’t always easy to express. What’s Rust’s solution?
That little lifetime marker '_
- we’ve all seen it. It’s used to express these dependencies at the API level.
Let’s see this in action. Everyone’s probably familiar with the Iterator
trait from the standard library. It looks like this:
pub trait Iterator {
type Item;
fn next(&'_ mut self) -> Option<Self::Item>;
// ...
}
Looks great, but there’s a tiny issue. The Item
type has no dependency at all on the type of the Iterator
itself (Self
). Why is that?
Because calling next
creates a temporary lifetime scope (the '_'
), which is a generic parameter on the next
function. Meanwhile, Item
is a standalone associated type - there's no way to tie it to that lifetime.
In most cases, this is not a problem. But in some libraries, this lack of expressiveness becomes a real limitation. Imagine an iterator that gives the user temporary files - they can close the file whenever they like. In this case, the Iterator
trait works fine.
But what if the iterator generates a temporary file, loads some data into it, and you need to delete the file after the user is done with it? Or better yet, reuse the storage space for the next file? In this case, the iterator needs to know when the user is done using the item.
This is exactly where GATs come in handy - we can use them to design an API like this:
pub trait StreamingIterator {
type Item<'a>;
fn next(&'_ mut self) -> Option<Self::Item<'_>>;
// ...
}
Now, the implementation can make Item
a dependent type, like a reference. The type system will ensure that before you call next
again or drop the iterator, the Item
value is no longer being used.
You’ve Been So Down-to-Earth - Can We Get a Bit More Abstract?
Alrighty then, from here on, let’s stop speaking human language. (Just kidding - but we’re going abstract now.) Note: this explanation is still simplified - for example, we’re going to set aside binders and predicates.
Let’s start by establishing the relationship between generic type constructors and concrete types. Essentially, it’s a mapping.
/// Pseudocode
fn generic_type_mapping(_: GenericTypeCtor, _: Vec<GenericArg>) -> Type;
For example, in Vec
, Vec
is the name of the generic type and also the constructor.
is the list of type arguments - just one in this case. When you feed both into the mapping, you get a specific type: Vec
.
Next up: traits. What is a trait, really? A trait is also a mapping.
/// Pseudocode
fn trait_mapping(_: Type, _: Trait) -> Option<Vec<AssociateItem>>;
Here, the Trait
can be thought of as a predicate - something that makes a judgment about a type. The result is either None
(meaning “does not implement this trait”), or Some(items)
(meaning “this type implements the trait”), along with a list of associated items.
/// Pseudocode
enum AssociateItem {
AssociateType(Name, Type),
GenericAssociateType(Name, GenericTypeCtor), // ← This is the new addition
AssociatedFunction(Name, Func),
GenericFunction(Name, GenericFunc),
AssociatedConst(Name, Const),
}
Of these, AssociateItem::GenericAssociateType
is currently the only place in Rust where generic_type_mapping
is indirectly invoked.
By passing different Type
s as the first parameter to trait_mapping
, you can get different GenericTypeCtor
s from the same Trait
. Then you apply generic_type_mapping
, and boom - you’ve combined different generic type constructors with specific Vec
arguments, all within Rust’s syntax framework!
A quick aside: constructs like GenericTypeCtor
are what some articles refer to as HKT - Higher-Kinded Types. Thanks to the approach described above, Rust now has user-facing support for HKT for the first time. Although it only appears in this one form, other usage patterns can be built from it.
In short: weird new powers unlocked!
Learning to Walk: Mimicking Advanced Constructs with GATs
Alright, to wrap up, let’s try using GATs to mimic some constructs from other languages.
#![feature(generic_associated_types)]
#![allow(incomplete_features)]
trait FunctorFamily {
type Type<T>;
fn fmap<T, U, F>(value: Self::Type<T>, f: F) -> Self::Type<U>
where
F: FnMut(T) -> U;
}
trait ApplicativeFamily: FunctorFamily {
fn pure<T>(inner: T) -> Self::Type<T>;
fn apply<T, U, F>(value: Self::Type<T>, f: Self::Type<F>) -> Self::Type<U>
where
F: FnMut(T) -> U;
}
trait MonadFamily: ApplicativeFamily {
fn bind<T, U, F>(value: Self::Type<T>, f: F) -> Self::Type<U>
where
F: FnMut(T) -> Self::Type<U>;
}
Now, let’s implement these traits for a specific “selector”:
struct OptionType;
impl FunctorFamily for OptionType {
type Type<T> = Option<T>;
fn fmap<T, U, F>(value: Self::Type<T>, f: F) -> Self::Type<U>
where
F: FnMut(T) -> U,
{
value.map(f)
}
}
impl ApplicativeFamily for OptionType {
fn pure<T>(inner: T) -> Self::Type<T> {
Some(inner)
}
fn apply<T, U, F>(value: Self::Type<T>, f: Self::Type<F>) -> Self::Type<U>
where
F: FnMut(T) -> U,
{
value.zip(f).map(|(v, mut f)| f(v))
}
}
impl MonadFamily for OptionType {
fn bind<T, U, F>(value: Self::Type<T>, f: F) -> Self::Type<U>
where
F: FnMut(T) -> Self::Type<U>,
{
value.and_then(f)
}
}
Alrighty, now we can use OptionType
as a “selector” to express and implement the behavior of Option
as a Functor, Applicative, and Monad.
So - how does it feel? Have we just unlocked a whole new world of possibilities?
We are Leapcell, your top choice for hosting Rust projects.
Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:
Multi-Language Support
- Develop with Node.js, Python, Go, or Rust.
Deploy unlimited projects for free
- pay only for usage — no requests, no charges.
Unbeatable Cost Efficiency
- Pay-as-you-go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real-time metrics and logging for actionable insights.
Effortless Scalability and High Performance
- Auto-scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the Documentation!
Follow us on X: @LeapcellHQ