October 28, 2022·5 min read·engineeringrusttypeslifetimestraits

Generic associated types express a borrow per operation

The planned Rust 1.65 stabilization lets an associated type depend on a lifetime or another generic parameter. Lending iterators explain the benefit and the limitations documented before release.

An iterator that returns a mutable view into itself needs the view's lifetime tied to one call, so the next call cannot invalidate a view the caller still uses. A single associated item type cannot express every such relationship. Generic associated types let the item type take the lifetime of the current borrow as a parameter.

Jack Huey's 28 October stabilization announcement says GATs will be stable in Rust 1.65, scheduled for 3 November. The announcement is especially useful because it also documents the initial implementation's limitations. The feature is approaching stable release, rather than already being available in that release today.

I find Rust's semantics compelling when they let an API state the lifetime relationship its implementation actually needs. GATs interest me for that reason. They can express a family of borrowed results without requiring every operation to return an owned allocation simply to fit a less precise trait.

The associated type varies with a call's borrow

The original RFC 1598, started in April 2016, motivates the feature with streaming iteration. An implementation can return overlapping mutable windows over a buffer when each returned window lasts only as long as the borrow from the corresponding next call.

That is different from providing several independently retained mutable windows. If the windows overlap, permitting callers to keep all of them simultaneously would violate the intended exclusivity. The useful API lends one view under the current mutable borrow and requires that borrow to end before another incompatible operation.

The October announcement gives the associated type its own lifetime parameter. An illustrative trait can include the optional end-of-stream result familiar from iteration.

trait LendingIterator {
    type Item<'a>
    where
        Self: 'a;

    fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}

This is a description of the forthcoming capability, not code claimed to compile on today's stable compiler. The item family is selected by the implementation, while each operation supplies the lifetime of its own borrow. The result can therefore refer to storage owned by the iterator itself.

The lifetime belongs on the associated item

The distinction between a trait lifetime and an associated-type lifetime matters. A trait generic over one lifetime describes a choice made at that trait instantiation. A generic associated type lets a single implementation provide results for the lifetimes requested by individual operations.

The RFC discusses a type-parameter form as well. An implementation can select a pointer family whose member type depends on another type parameter. This is a specific extension to where generics can appear, rather than a promise of every abstraction sometimes grouped under higher-kinded types.

That scope keeps the practical question manageable. The feature permits a trait's associated type to be parameterized. It does not imply that arbitrary type constructors can be passed around wherever ordinary type parameters are accepted. An API should be assessed against the capability being stabilized, not a larger type-theory label.

My preference is to introduce the feature through the relationship it enables. A borrowed result varying per call is easier to understand than a broad claim that Rust has acquired a new category of abstraction. The declaration then supplies the exact parameters involved.

Required bounds preserve possible implementations

The announcement explains the where Self: 'a clause in the lending example. Borrowing self for that lifetime establishes that Self outlives it. The associated type needs the corresponding bound on the trait so implementations can use that relationship.

An implementation cannot simply add a stricter requirement absent from the trait. Omitting the clause from the public declaration would rule out useful implementations that need it. The compiler therefore requires relevant bounds based on the methods that use the associated type.

This is a public API design issue as well as a syntax requirement. The trait author declares which families implementations may provide and under which assumptions callers may use them. A missing relationship can reduce implementation flexibility even when the first concrete implementation seems straightforward.

The announcement's diagnostic explicitly suggests the required clause. That is useful guidance for the author, while the reason for the clause remains the method's borrowing relationship. Treating it as unexplained boilerplate makes the next trait harder to design correctly.

Higher-ranked bounds can expose an implementation bug

One documented limitation concerns a bound requiring the item family to implement a trait for every relevant lifetime. The announcement demonstrates this with Debug and a lending iterator over mutable windows. The current implementation can incorrectly imply a static lifetime, rejecting borrowed local data that should be acceptable.

That failure is distinct from a program legitimately asking for a static reference. A diagnostic mentioning a local value that does not live long enough can come from this implementation limitation. The announcement explains the affected shape of bound so users can recognize the case.

It also shows a borrow-checker limitation in a lending filter loop. An item returned in one branch causes the checker to treat a prior mutable borrow as still relevant on a later iteration. The source example describes valid intended behavior that the current checker cannot accept.

Those limitations matter for library composition. Expressing the core trait does not guarantee that every familiar iterator adapter can be translated mechanically. An abstraction can be expressible while one of its generic consumers still encounters an implementation restriction.

Trait objects need another implementation step

The announcement says traits with GATs are not object safe in the initial stabilization. Specifying the associated item only for a static lifetime would not define its value for all lifetimes a caller might use. Supporting the full family through a trait object needs additional trait-solver work.

This limits how a library can expose dynamic dispatch around such traits. A successful generic implementation should not be assumed to admit a corresponding dyn interface. Choosing the public dispatch model remains part of evaluating the proposed API.

I regard the limitation report as a useful part of the stabilization announcement. It lets library authors separate the relationship the language is adding from the combinations its current implementation cannot yet support. A lending operation's returned type can depend on the borrow of this call, while the caller must still respect the lifetime of the view it receives.