The latest release of Rust introduces a powerful new language feature, called generic associated types, that allows developers to specify generics on associated types in traits. Other notable new features include the let-else
statement, and support for break
ing out of labeled blocks.
Over six years in the making, generic associated types (GATs) can be thought of as a form of type constructors on traits and can be used to define generic type, lifetime, or const generics on associated types.
This specific feature (associated type constructors) resolves one of the most common use cases for higher-kindedness, is a relatively simple extension to the type system compared to other forms of higher-kinded polymorphism, and is forward compatible with more complex forms of higher-kinded polymorphism that may be introduced in the future.
Associated types in Rust are a mechanism to defined generic traits. For example, the following example shows a Graph
trait using two associated types for its nodes and edges:
trait Graph {
type N;
type E;
fn has_edge(&self, _: &Self::N, _: &Self::N) -> bool;
fn edges(&self, _: &Self::N) -> Vec<Self::E>
}
The use of associated types improves the readability of code and conveys the idea of a family of related types. A client of the Graph
trait can indeed use it without the need to specify each time what its associated types are, as it would be required for a generic type. For example:
fn distance<C: Contains>(graph: &G, start: &G::N, end: &G::N) -> i32 { ... }
Now, GTAs introduce a way to specify an associated type that is itself generic. For example:
trait LendingIterator {
type Item<'a> where Self: 'a;
fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}
Compare this with the definition of the standard Iterator
:
pub trait Iterator {
type Item;
...
fn next(&mut self) -> Option<Self::Item>
...
}
As you can see, LendingIterator
uses Item<'a>
instead of the non-generic Item
associated type and constrains Self
to be of type 'a
. This means that the next
function returns an item that borrows from self
.
While somewhat arcane at first sight, GATs are a very powerful abstraction that is already used in its unstable incarnation by many crates. Additionally, a number of other crates were blocked in their further development by GATs not being stable. A few examples of the kind of features that can be built using GATs are zero-copy interfaces to load data from a DB, generic build patterns, representing non-owned values, and many more. It is also worth noting that in some cases GATs were replaced by the use of unsafe code and that GATs will reduce the need for unsafe code, where Rust stops providing any safety guarantees, in those cases.
As mentioned, GATs are not the only notable new feature in Rust 1.65. Specifically, the new let-else
statement and early block termination using break
are also worth a mention.
A let-else
statement is an extension of a let
that attempts to match a pattern and additionally provides and else
block to be executed when there is no match, for example to return early from a function or panic
.
Labeled break
s give full dignity to a behavior that was already achieved using the workaround of loop
blocks that execute just once, due to the fact that loop
blocks support the possibility of arbitrarily breaking out of the loop. Now, you can also label a block and then use the break <block-label>
statement to jump at the end of that labeled block.
If you are interested in the full details about changes in Rust 1.65, check the official release notes.