May 10, 2018·6 min read·engineeringrusttraitslanguage-design

Rust 1.26 returns hidden concrete types with impl Trait

impl Trait lets an API hide a concrete return type without requiring a boxed trait object. Rust 1.26 also simplifies matching borrowed values while preserving borrowing rules.

An iterator pipeline can be easy to read as an expression and difficult to name as a return type. Each adapter adds another concrete type, and a closure introduces a type the programmer cannot write by name. Rust 1.26 gives that function a signature that promises iterator behavior without exposing the entire construction. The returned value keeps its concrete representation and static dispatch.

Today's release announcement stabilizes impl Trait alongside changes to pattern matching and ranges. The return-type feature addresses a specific tension in Rust's abstractions. An API should be able to describe what its caller may do without forcing the implementation to allocate a trait object merely to avoid naming an unwieldy type.

I find this a strong fit for Rust's systems role. An abstraction can improve the contract visible to a caller while retaining the representation the compiler already understands. The useful part is the separation between the public promise and the implementation's concrete type.

The function chooses the hidden return type

RFC 1522 describes the conservative abstract-return-type design. A function returning impl Trait produces one concrete type that satisfies the declared bound. The caller uses the bound without receiving a source-level name for that hidden type.

An iterator makes the effect concrete.

fn doubled() -> impl Iterator<Item = u32> {
    (0..4).map(|n| n * 2)
}

fn main() {
    let values: Vec<u32> = doubled().collect();
    assert_eq!(values, vec![0, 2, 4, 6]);
}

The function promises an iterator of unsigned integers. Its implementation is an adapter over a range. A caller collecting its values need not know the adapter's full concrete type. There is no boxed trait object in this function, and hiding the type does not introduce one.

The distinction improves API maintenance. Exposing a particular adapter type can expose the sequence of operations used to construct it. A function that promises only an iterator leaves more freedom to change that sequence while preserving the declared behavior. The caller receives the operations expressed by the bound instead of depending on the implementation's spelling.

That freedom has limits. Changing the values produced by an iterator can still violate an API's documented behavior even when its trait bound stays the same. A type signature describes part of a contract. It does not replace the explanation of what the function computes.

The RFC also explains that auto traits such as Send can remain observable through the hidden type. An implementation change that removes such a property can affect callers even when the written return bound is unchanged. Hiding the adapter name therefore does not make every implementation substitution compatible. Library authors still need to account for the properties their returned value supplies to consumers, including whether it can cross a thread boundary.

Different branches still need one return type

The hidden type is concrete, which means all returns from the function must agree on it. Returning a filtered iterator in one branch and the unfiltered iterator in another does not satisfy that rule just because both implement Iterator. Their adapter types differ.

The release announcement illustrates this boundary with an iterator selected by a runtime condition. Its example uses a boxed trait object when the two possible results have different concrete types. That remains an appropriate use for dynamic dispatch. impl Trait does not make heterogeneous runtime choices disappear.

This is a useful distinction for designing an API. A single implementation type hidden behind a bound solves the problem of expressing a complicated result. A trait object solves a different problem by allowing different concrete implementations behind a runtime interface. They can present similar behavior to the caller while making different representation choices.

An author can also redesign an implementation so its branches produce the same concrete type. Whether that is worthwhile depends on the operation. The language feature does not require such a redesign, and its existence is not a reason to remove trait objects from a program that needs runtime variation.

Argument position has a different choice

Rust 1.26 also permits impl Trait in argument position. There it expresses an accepted generic input satisfying a bound. The caller supplies the concrete type. In return position, the function implementation chooses it. The surface spelling is similar, but the direction of that choice differs.

This matters when reading a signature. An argument bound does not promise that a function returns the same hidden type, nor does a return bound let a caller request any implementation it prefers. The role of the type in the function's contract determines who chooses it.

The original conservative RFC focused on return types, while the released feature also covers arguments. That is another reason to read the release description alongside the design document. The design history explains the initial narrow scope, and the release establishes which parts are now usable on stable Rust.

I prefer using the spelling where it shortens a signature without concealing a relationship the reader needs. Named type parameters remain useful when the API must express relationships among arguments. A short bound is sufficient when one input simply needs to implement a behavior.

Matching a borrowed value needs fewer markers

The other large ergonomic change concerns patterns over references. RFC 2005 explains the earlier friction. Matching a borrowed enum could require an explicit reference pattern and ref bindings even when moving the inner value was impossible. Authors often followed compiler suggestions through several edits to express an ordinary borrow.

Rust 1.26 adjusts the default binding mode while matching a reference with a non-reference pattern. Inner bindings then borrow appropriately. The release shows matching an &Option<String> with Some(name) and receiving a borrowed name, without the earlier &Some(ref name) spelling.

The important property is that the shorter pattern does not grant ownership of the string. Borrowing rules remain in force. A pattern that inspects a shared reference cannot silently move out the owned value behind it. Understanding the binding as a borrow explains the behavior better than treating the compiler's assistance as arbitrary automatic conversion.

The RFC also describes mutable-reference matching. The selected default mode can produce a mutable borrow when the input permits it. That preserves the distinction between shared and mutable access while reducing repeated pattern syntax. Existing explicit forms remain part of the language.

Inclusive ranges express a maximum directly

The new ..= range syntax includes its upper endpoint. This is especially useful when an integer type cannot represent one more than the largest desired value. A half-open range usually expresses a stop value after the last included element, but that value may not exist within the chosen integer type.

The release announcement uses the full range of u8 as its example. An inclusive upper bound of 255 expresses the intended maximum without trying to represent 256 as a u8. The new notation therefore addresses a concrete representational problem, as well as making an inclusive intention visible to a reader.

This does not change the meaning of existing half-open ranges. An author still chooses whether the endpoint belongs to the iteration. The language now has a direct spelling for both decisions, so code does not need to manipulate the endpoint solely to obtain inclusion.

Slice patterns preserve the structure being inspected

Basic slice patterns add another way to state a program's conditions directly. The release demonstrates branches for slices of different lengths, with a fallback covering all other lengths. A successful pattern can identify both the length and the positions that a branch requires.

This is useful for small structured inputs. A parser checking for exactly two elements can bind those elements in the same operation that establishes the length. Its branch no longer needs a separate length check followed by indexing that relies on the earlier condition. The pattern connects the evidence with the values it makes available.

The feature is described as basic slice patterns, so it should not be read as a promise that every possible slice-pattern extension is stable. The available forms already cover concrete fixed-length cases. An exhaustive fallback remains necessary when the input length is not statically fixed and the named cases do not cover every possibility.

Rust 1.26 consequently changes several ways a program expresses information the compiler can already reason about. A return signature can promise an iterator without naming its adapters. A borrowed enum can be matched without repeating reference markers, and a fixed-length slice pattern can establish the positions its branch uses. Each change reduces a specific form of source work while preserving a concrete type or borrowing requirement beneath it.