A new impl Trait 3/4
The current state of impl Trait
May 12, 2022 – 9 min read
Rust
Rust

If you're not interested in reading about the prior work done on impl Trait, feel free to skip ahead to the fourth and final post.

Prior work

The concepts behind impl Trait have been around for a surprisingly long time. I've done my best to collect relevant RFCs, successful and failed, review them and compare them to my proposal. There are five RFCs that are directly relevant to impl Trait and type abstraction. Let's go in chronological order.

The original impl Trait

The first impl Trait RFC from @aturon was not accepted, but a proposed more conservative version of it was accepted.

In many ways, this RFC has the same goals as my recommendation. Specifically, it addresses the following by name:

It even proposed allowing impl Trait in any type position, which as impl Trait also aims to allow. Thare are places in the thread where as impl Trait is even used, albeit in the context of a value. So why was the more conservative version accepted instead of the original?

impl Trait anywhere

The original proposal didn't have a concrete use-case for impl Trait outside of function signatures. With a more current need for TAIT, I would say that we now have a motivating use case for it.

Named return types

The original proposal suggested syntax like collect_to_set::<T, I>::impl to refer to the impl Trait return type of the function collect_to_set. This raised questions about how to refer to nested return types. as impl Trait does not have this problem, since the correct approach in this case would be to separate the return type into a type alias. It is good to question how named unnameable types interact with distant type aliases though and whether this would be an issue. Nested as impl Traits can be approached in the same way.

Trait object confusion

There was a lot of concern that it would be easy to confuse impl Trait (a statically-dispatched abstract type) with Trait. At the time, Trait was the syntax for trait objects. It has since been replaced with dyn Trait and deprecated, so the risk of confusion is lower now than ever. Additionally, I think that Type as impl Trait conveys a much larger difference from dyn Trait than impl Trait does.

impl Trait is not powerful enough

impl Trait was proposed as being a very general-purpose equivalent of existential types, which as impl Trait does not. There was some reluctance to having two ways to express the same concept, which isn't as much of a problem with as impl Trait.

Coupled sugarings

@pnkfelix actually calls this out explicitly in a comment, how impl Trait has two different uses depending on whether it's used in argument position or return position. There's also discussion about how covariant and contravariant appearances of impl Trait have different desugarings, and even a suggestion that some Trait be introduced to capture that notion. This parallels a lot of what was discussed in the first post.

Leaky auto traits

This was a controversial aspect at the time, but it's now well-known that abstractions leak auto traits. This hasn't changed, and the previous discussion is extensive and not worth rehashing.

Conclusion

At the end of it all, @aturon closed the RFC in favor of working towards some alternatives with other interested members.

This first RFC does somewhat of a whirlwind tour of the problems that impl Trait attempts to solve. Existential types made an appearance, conditional trait bounds showed up, and there was speculative syntax galore. It properly sets the stage for:

Conservative impl Trait

This RFC is essentially just a limited version of the original proposal. It only allows impl Trait in argument and return position on free and inherent functions. Unfortunately, there's not much to comment on here since it's a subset of the previous RFC. Some notes:

Abstract vs anonymous

There are a few places here where the terminology around these types are questioned. I prefer "abstract" to draw a clearer separation between abstracted types and unnameable types, both of which could be confused as "anonymous" by non-experts (sorry @eddyb!).

Named output types

@eddyb suggests naming the return types of functions, which is very similar to my suggestion for named unnameable types. Even down to the use of the type keyword, which was fun to see. Naming the return types of functions alone would make it possible to do everything that type alias as impl Trait can do, but using it in conjunction with type aliases would give us incredibly comfy ergonomics.

Anxiety of impl Trait overuse

The downsides of impl Trait are known, and a few people pointed out that encouraging the use of impl Trait in more places could lead to undesirable situations. We discussed this problem when we talked about whether we want our trait implementations to leak, and I think that as impl Trait alleviates many of these concerns by preserving the concrete type information.

Bikeshedding: impl vs type

There seem to be some strong opinions about which syntax would be better. impl Trait won evidently.

An unclear future for impl Trait

A really poignant critique of the RFC was that the original failed because many people wanted different futures for it. This RFC was effectively just the lowest common denominator, but doesn't resolve the question of which future impl Trait should have. This is exactly the same problem that I aim to address with as impl Trait.

for blocks

@glaebhoerl suggests using for blocks to desugar return-position impl Trait for generic free functions. It's a cool idea, but doesn't get much further exploration as the RFC period was winding down.

Expanded impl Trait

This RFC, in essence, took on all the bikeshedding that wasn't ultimately essential to the first RFC. It does manage to introduce impl Trait in argument position, resolve bikeshedding around syntax, and come to a conclusion about type and lifetime parameters interacting with impl Trait.

There's honestly not too much to discuss here that hasn't already been discussed. Most of this RFC is consensus building and cornering impl Trait to prevent it from getting out of hand. This is a valuable RFC to read if you want to understand the specific semantics of impl Trait.

impl Trait existential types

We've finally caught up and are moving on to the more experimental RFCs. This RFC aims to introduce existential types and expand impl Trait to more places. I have some real critiques of this RFC, so let's get into them.

as impl Trait in spirit

One of the first sections of this RFC suggests this syntax for abstracted types:

let displayable: impl Display = "Hello, world!";

This doesn't include the concrete underlying type, and so I think this would be much clearer as:

let displayable: &'static str as impl Display = "Hello, world!";
// or, with variable type inference
let displayable: _ as impl Display = "Hello, world!";

This addresses the same problems, but with an arguably clearer syntax:

// Concrete
const DISPLAYABLE: &'static str = "Hello, world!";
// Abstract
const DISPLAYABLE: &'static str as impl Display = "Hello, world!";

And can handle unnameable types with local inference:

const MY_CLOSURE: _ as impl Fn(i32) -> i32 = |x| x + 1;

Existential types

This section in particular muddies the water with impl Trait. The introduction of existential types is intended to provide the same type-abstracting functionality as impl Trait, but with the added benefit of being usable in more positions. There is a particular focus on type aliases and associated types, where the concrete underlying type is inferred based on its use.

I believe that inferring this concrete type is a mistake, and leads to the same inference issues discussed in the last post. Additionally, there is a large focus on not naming the concrete type to prevent trait impls from leaking. However I think this confuses the abstraction of a type with the naming of it. It's clearer to name a type and explicitly abstract it.

Fundamentally, I believe that existential types are a less clear formulation of as impl Trait. That's a very subjective opinion.

Type alias impl trait

Finally, this RFC is nice and light. It simply builds on the existential types RFC by explicitly setting the syntax to use impl Trait. There is a lot of language lawyering and nailing down the very specific semantics, and it does a good job of explaining why additional syntax hinders learnability and results in confusion. We've already discussed the pros and cons of this RFC extensively, and I think we can do better.

Conclusion

impl Trait has taken a long journey to reach where it is now. Along the way there has been great ambition, as well as great confusion. I understand that there are very strong convictions on both sides, and I hope that we can use this as an opportunity to finally resolve them.

In my final post, I hope to bring the past three posts together into a coherent framework and provide a final recommendation on what should be done with impl Trait. If you've made it this far, I appreciate it a lot and hope you can hang on for just a little while longer.