Impl trait initiative
What is this?
This page tracks the work of the Impl trait initiative! To learn more about what we are trying to do, and to find out the people who are doing it, take a look at the charter.
Current status
This is an umbrella initiative and, as such, it covers a number of subprojects.
Subproject | Issue | Progress | State | Stage |
---|---|---|---|---|
impl Trait in fns and inherent methods | #123 | ▰▰▰▰▰▰ | ✅ | Stabilized |
type alias impl trait | #63063 | ▰▰▰▰▱▱ | 🦀 | Development |
impl trait in traits, impls | #123 | ▰▰▱▱▱▱ | 🦀 | Experimental |
impl trait in bindings | #63065 | ▰▰▰▱▱▱ | 💤 | Development |
impl trait with multiple types | ▰▱▱▱▱▱ | 💤 | Experimental | |
existential lifetimes | ▰▱▱▱▱▱ | 💤 | Experimental |
Key:
- ✅ -- project complete
- 🦀 -- project in progress
- 💤 -- project not under active development
How Can I Get Involved?
- Check for 'help wanted' issues on this repository!
- If you would like to help with development, please contact the owner to find out if there are things that need doing.
- If you would like to help with the design, check the list of active design questions first.
- If you have questions about the design, you can file an issue, but be sure to check the FAQ or the design-questions first to see if there is already something that covers your topic.
- If you are using the feature and would like to provide feedback about your experiences, please [open a "experience report" issue].
- If you are using the feature and would like to report a bug, please open a regular issue.
We also participate on Zulip, feel free to introduce yourself over there and ask us any questions you have.
Building Documentation
This repository is also an mdbook project. You can view and build it using the following command.
mdbook serve
📜 Impl trait Charter
Proposal
impl-trait is an umbrella initiative that is generally developing the impl Trait
syntax in Rust. The overall goal is to enable users to write impl Trait
to mean "some type implemementing Trait
". We wish to enable this syntax in as many places as it makes sense to do so, and to develop a coherent philosophy around its meaning.
Membership
Role | Github |
---|---|
Owner | nikomatsakis |
Liaison | ghost |
✏️ Updates
Lang-team initiatives give monthly updates. This section collects the updates from this initiative for posterity.
2021-Oct: Lang team update
Summary
- oli-obk completed a rewrite of the inference engine and is in the process of landing it.
- oli-obk is also pursuing a rewrite of the region checker to use intersection regions to address some of the shortcomings in the current system.
- spastorino wrote a number of test cases covering the "feature surface area".
- spastorino removed the (incorrectly implemented) support for
let x: impl Trait
; it will be re-evaluated later. - nikomatsakis is working on an explainer covering the "big picture" of TAITs.
- tmandry + nikomatsakis are planning a proposal to permit users to name the types of associated functions from traits or inherent impls.
- This opens the way for
-> impl Trait
notation in traits.
- This opens the way for
Goals for this month
- Prepare stabilization report
- Land oli's rewrite
- Extend explainer
- Extend evaluation doc to cover naming of impl traits
Questions for discussion
- How long should we allow new inference engine to "bake" before stabilizing?
- We have a large suite of test cases.
- The inference engine is used for all impl Trait, including
-> impl Trait
andasync fn
, so it gets a lot of testing once it hits stable.
Type alias impl trait stabilization
The major focus remains "type alias impl trait" (TAIT) stabilization:
#![allow(unused)] fn main() { type Foo = impl Trait; impl SomeTrait for SomeType { type AssocType = impl Debug; ... } }
Inference engine rewrite
oli-obk has completed a rewrite of the TAIT inference engine so that it matches the inference behavior of the RFC. This rewrite has been largely completed on a branch and is in the process of landing. It requires a number of supporting changes, such as improvements to NLL diagnostics.
The semantics that were implemented are briefly defined as follows. First, each TAIT has a "defining scope" based on where it appears:
- In a module-level type alias: the defining scope is the enclosing module.
- In the value of an associated type: the defining scope is the impl.
Within that defining scope, each item (function, etc) that references the TAIT defines a value for the TAIT based on the results of type-checking. Essentially, the TAIT's value is assumed at first to be an unbound inference variable, and we solve for whatever value of that variable will make the function type check. Each item must completely define the hidden type without relying on other items; further, each item must provide the same value for the hidden type. Example:
#![allow(unused)] fn main() { mod tait { pub type TAIT = impl Debug; // `foo` infers TAIT to be `u32`, // because that is the value needed // to make it type check: pub fn foo() { let x: TAIT = 22_u32; } // `get` also infers TAIT to be `u32`, // because that is the value needed // to make it type check: pub fn get() -> TAIT { 22_u32; } // `take` also infers TAIT to be `u32`, // because that is the value needed // to make it type check. Calling `take` // from *outside* the module would not // be possible except by providing a return // value from `get`. pub fn take(x: TAIT) { let y: u32 = x; } // Other also constraints TAIT to be `u32`, // albeit indirectly. pub fn other() { take(22_u32); } } }
Outside of the defining scope, references to the type TAIT are opaque. The code is not permitted to rely on the hidden type, though it may peek at the auto traits implemented by the hidden type:
mod tait { ... /* as above */ ... } fn main() { // Allowed tait::take(tait::get()); // ERROR-- The hidden type is not visible outside of // the module `tait`, so we can't rely on it being // `u32`. tait::take(44_u32); // ok -- TAIT is an `impl Debug` is_debug::<tait::TAIT>(); // ok -- auto traits can rely on the hidden type, // and `u32: Send` is_send::<tait::TAIT>(); // error -- even though `u32` implements `Display`, // we don't know the hidden type is_display::<tait::TAIT>(); } fn is_send<T: Send>() { } fn is_debug<T: Debug>() { } fn is_display<T: Display>() { }
Naming impl Trait in traits
We would like to support -> impl Trait
in traits and impls. One of the questions that has been blocking progress is the question of how to name the resulting associated type. In other words, the syntax
#![allow(unused)] fn main() { trait Foo { fn bar(&self) -> impl Debug + '_; } }
is equivalent to something like this but where Bar
is an implicit associated type
#![allow(unused)] fn main() { trait Foo { type Bar<'me> = impl Debug + 'me; fn bar(&self) -> Self::Bar<'_>; } }
The problem is that users have no way to name Bar
, and hence no way to name the return type of bar
.
There have been numerous proposals to address this, but the most straightforward -- and also the most enabling -- seems to be allowing users to get access to the "zero-sized fn type" that represents bar
.
The idea would be to define an associated type for every fn item, so that Foo::bar
identifies the associated type for this fn in general, and <T as Foo>::bar
identifies the associated type for the function as implemented for the type T
.
This permits you to access the return type by writing T::bar::Output
, for example, although we will have to extend the type checker to accommodate relative associated types like that (the full syntax would be <<T as Foo>::bar as FnOnce>::Output
).
These are the known questions to address in the evaluation doc:
- Can you name top-level functions or other sorts of functions?
- What about the constructor functions for tuple structs or enum variants?
- How to manage associated types and fns with the same name?
- Do we have an unambiguous syntax such as
T::fn(bar)
to resolve that?- (This might apply to constructor functions as well.)
- Do we have an unambiguous syntax such as
🔬 Evaluation
The evaluation surveys various specific questions that have arisen over the course of the impl trait design and gives a detailed write-up of the alternatives that were considered. See the table of contents on the left for the list of questions.
APIT and Turbofish
Summary
Introduction
impl Trait
in argument position introduces a new generic parameter to the function:
#![allow(unused)] fn main() { fn collect_to_vec<T>(iter: impl Iterator<Item = T>) -> Vec<T> { iter.collect() } // is roughly equivalent to: fn collect_to_vec_desugared<T, I>(iter: I) -> Vec<T> where I: Iterator<Item = T>, { iter.collect() } }
With the desugared version, users can write an expression that manually specifies the values for its type parameters:
#![allow(unused)] fn main() { let f = collect_to_vec_desugared::<u32, vec::IntoIter<u32>>; }
The question addressed here is whether it should be possible to write an equivalent expression for collect_to_vec
.
Current behavior
The compiler today prevents the use of turbofish for any function with APIT. This is a future compatibility restriction and not a long term plan.
Observations
Users should not have to acknowledge impl Trait during turbofish, that'd be confusing
Today, when using turbofish form, we require that users write a value for all generic type parameters, even if that value is _
(we allow lifetime parameters to be elided if the user chooses, and in fact require them to be elided in some cases). Therefore, it would be an error to write collect_to_vec_desugared::<u32>(vec.into_iter())
. It is allowed to write collect_to_vec_desugared::<u32, _>(vec.into_iter())
, however, and thus to lean on type inference for one parameter.
While APIT is "roughly equivalent" to a new generic parameter, this parameter is not listed explicitly in the list of generics. Therefore, it would likely be surprising for users to get an error for not giving it a value. This argues that collect_to_vec::<u32>(vec.into_iter())
should be accepted.
If we wish to allow turbofish to specify the value for an impl Trait, we would need to linearize the list
Furthermore, if we wished to extend impl Trait
parameters into the list of type parameters, we would have to come up with an ordering. Presumably it would be "left to right" as they are written within the arguments.
Some type parameters are given explicit values more often than others and Rust doesn't let one make that distinction
It sometimes happens that functions have some type parameters which are useful (even mandatory) to specify, and others which are not. This usually happens when some type parameters appear in the argument list, and hence can be inferred from the call arguments, but others either do not appear anywhere or appear only in the return type. Example:
#![allow(unused)] fn main() { fn log_the_things<D: DebugLog>(strings: impl IntoIterator<Item = String>) { for s in strings { D::log_str(s); } } }
Here, it would be common for users to wish to write something like log_the_things::<SomeLoggerType>(some_vector)
, where the type D
is manually specified but the value of the impl IntoIterator
is inferred from the argument. Without using impl Trait
, this would have to be written log_the_things::<SomeLoggerType, _>(...)
.
You sometimes want to get a function type without calling it
In all the examples so far, we have assumed that it was possible to infer the value of an impl Trait
argument from the arguments that were supplied. But that might not always be true. Consider this rather artificial example:
#![allow(unused)] fn main() { struct Foo<T> { t: T } fn test_fn(x: impl Iterator) { } fn make() -> Foo<impl Debug> { Foo { x: test_fn } } }
Here, we are not calling test_fn
, just taking its value.
Alternatives
Allow users to the values for impl Traits, but do not require it
📚 Explainer
The "explainer" is "end-user readable" documentation that explains how to use the feature being developed by this initiative. If you want to experiment with the feature, you've come to the right place. Until the feature enters "feature complete" form, the explainer should be considered a work-in-progress.
Overview
This explainer describes the "impl Trait" feature as a complete unit. However, not all parts of this story are in the same state. Some parts are available on stable, others are available on nightly, others have accepted RFCs but are not yet implemented, and still others are in the exploration phase. Each section of the explainer has a "status" tag that indicates whether the status of that particular content.
Impl trait: make the compiler do the hard work of figuring out the exact type
Status: Stable
Oftentimes, you have a situation where you don't care exactly what type something has, you simply care that it implements particular traits. For example, you might have a function that accepts any kind of iterator as long as it produces integers. In this case, you could write the following:
#![allow(unused)] fn main() { fn sum_integers(integers: impl Iterator<Item = u32>) -> u32 { // ^^^^^^^^^^^^^ let mut sum = 0; for integer in integers { sum += x; } sum } }
Here, the impl Iterator
type is a special sort of type. It doesn't actually name any particular type. Instead, it is a shorthand for a generic type. In other words, the function above is roughly equivalent to the following desugared version:
#![allow(unused)] fn main() { fn sum_integers<I>(integers: I) -> u32 where I: Iterator<Item = u32> { let mut sum = 0; for integer in integers { sum += x; } sum } }
Intuitively, a function that has an argument of type impl Iterator
is saying "you can give me any sort of iterator that you like".
Impl trait in argument types
When you use an impl Trait
in the type of a function argument, that is generally equivalent to adding a generic parameter to the function.
So this function:
#![allow(unused)] fn main() { fn sum_integers(integers: impl Iterator<Item = u32>) -> u32 { // ^^^^^^^^^^^^^ let mut sum = 0; for integer in integers { sum += x; } sum } }
is roughly equivalent to the following generic function:
#![allow(unused)] fn main() { fn sum_integers<I>(integers: I) -> u32 where I: Iterator<Item = u32> { let mut sum = 0; for integer in integers { sum += x; } sum } }
Intuitively, a function that has an argument of type impl Iterator
is saying "you can give me any sort of iterator that you like".
Turbofish
Using impl Trait
in argument position conceptually adds a generic parameter to the function. However, this generic parameter is different from explicit, named parameters. Its value cannot be specified explicitly using the turbofish operator, and must be inferred. Therefore, these two functions behave in slightly different ways:
#![allow(unused)] fn main() { fn foo1(x: impl Clone) { } fn foo2<C: Clone>(x: C) { } }
The difference is that one can write foo2::<i32>
to manually specify the value of the C
parameter, but the value of the impl Clone
parameter on foo1
must be inferred from the argument types that are supplied (e.g, foo1(22)
).
It is possible to mix explicit generics with impl Trait:
#![allow(unused)] fn main() { fn foo3<A: Debug>(x: A, b: impl Clone) { } }
In this case, one can use turbofish to specify the value of A
, but not the impl Clone
. In other words, one could write foo3::<i32>
to get a function where A
is specified as i32
, but the value of impl Clone
will still have to be inferred from usage.
Implication. The fact that impl trait and explicit generics are not equivalent means that one cannot migrate between them in a semver-compliant way, although breakage is quite unusual in practice.
Implementation note: This section describes the "recommended" behavior that we expect to propose to the lang team. The currently implemented semantics are different. They forbid turbofish from being used on any function that employs argument position impl trait.
Traits and impls
You can use impl Trait
in argument position in traits and impls, but you must use it consistently in both. For example, the following is legal:
#![allow(unused)] fn main() { trait Operation { fn compute(x: impl Iterator<Item = u32>) -> u32; } struct Sum; impl Operation for Sum { fn compute(x: impl Iterator<Item = u32>) -> u32 { x.sum() } } }
But the following would be illegal:
#![allow(unused)] fn main() { struct Max; impl Operation for Max { fn compute<I: Iterator<Item = u32>>(x: I) -> u32 { x.max() } } }
Impl trait in type aliases
Rust has a concept of type aliases, which let you declare one type name as an alias for another:
#![allow(unused)] fn main() { type Integer = i32; }
Type aliases can be useful for giving a short alias for some complex type. For example, imagine we had a module odd
that defined an odd_integers
function. odd_integers(x, y)
returns an iterator over all the odd integers between x
and y
. Because the full return type is fairly complicated, the module defines a type alias OddIntegers
that people can use to refer to it.
#![allow(unused)] fn main() { mod odd { pub type OddIntegers = std::iter::StepBy<std::ops::Range<u32>>; pub fn odd_integers(mut start: u32, stop: u32) -> OddIntegers { if (start % 2) == 0 { start += 1; } (start..stop).step_by(2) } } }
Of course, typing that long type is kind of annoying. Moreover, there are some types in Rust that don't have explicit names. For example, another way to write the "odd integers" iterator would be to call filter
with a closure. But if you try to write out the type for that, you'll find that you need to give a name to the type representing the closure itself, which you can't do!
#![allow(unused)] fn main() { mod odd { pub type OddIntegers = std::iter::Filter<std::ops::Range<u32>, /* what goes here? */>; pub fn odd_integers(start: u32, stop: u32) -> OddIntegers { (start..stop).filter(|i| i % 2 != 0) } } }
Enter impl Trait1
"He says in parentheses"
The truth is that specifying the exact type for OddIntegers
is overkill anyway. Chances are that we don't actually care what exact type is used there, we only care that odd_integers
returns some kind of iterator. In fact, we might prefer not to specify it: that gives us the freedom to change how odd_integers
is implemented in the future without potentially affecting our callers. If this sounds familiar, that's good -- it's exactly the kind of scenario that impl Trait
is meant to solve!
Using impl Trait
, we can still have a type alias OddIntegers
, but we can avoid specifying exactly what its value is. Instead, we just say that it is "some iterator over u32":
#![allow(unused)] fn main() { mod odd { pub type OddIntegers = impl Iterator<Item = u32>; // ^^^^^^^^^^^^^^^^^^^^^^^^^ :tada:! pub fn odd_integers(mut start: u32, stop: u32) -> OddIntegers { if (start % 2) == 0 { start += 1; } (start..stop).step_by(2) } } }
Now it's the compiler's job to figure out the value of that type alias. In this case, the type for OddIntegers
would be inferred to StepBy<Range<u32>>
, but we could also change the definition to use filter
, in which case the hidden type would be Filter<Range<u32>, C>
, where C
represents the type of the closure. This shows something important: the value of an impl Trait
can include types that don't otherwise have names (in this case, the closure type C
).
Hidden types
You can see that impl trait behaves a bit differently depending on where you use it. What all positions have in common is that they stand in for "some type that implements the given trait". We call that type the hidden type, because you generally don't get to rely on exactly what it is, you only get to rely on the bounds that it satisfies. What distinguishes the various positions where you can use impl Trait is which code determines the hidden type:
Position | Who determines the hidden type |
---|---|
Argument position | The caller |
Type alias | Code within the enclosing module |
... (full table in Appendix B) |
Inferring the hidden type
When you use impl Trait
in a type alias, the hidden type is inferred based on the code in the enclosing module. We call this enclosing module the defining scope for the impl trait. In our example, the defining scope is the module odd
:
#![allow(unused)] fn main() { mod odd { pub type OddIntegers = impl Iterator<Item = u32>; pub fn odd_integers(mut start: u32, stop: u32) -> OddIntegers { .. } } }
As we type-check the functions within the module odd
(or its submodules), we look at the ways that they use the type OddIntegers
. From this we can derive a set of constraints on what the hidden type has to be. For example, the compiler computes that odd_integers
returns a value StepBy<Range<u32>>
, but the function is declared to return OddIntegers
: therefore we conclude that odd_integers
requires the hidden type to be StepBy<Range<u32>>
.
There can be multiple functions that constrain the type
Of course, modules can have more than one function! It's fine to have multiple functions that constrain the type OddIntegers
, so long as they all ultimately constrain it to be the same thing. It's also possible to constrain OddIntegers
by referencing it in other places. For example, consider the function another_function
here:
#![allow(unused)] fn main() { mod odd { pub type OddIntegers = impl Iterator<Item = u32>; // Requires `OddIntegers` to be `StepBy<Range<u32>>` pub fn odd_integers(mut start: u32, stop: u32) -> OddIntegers {..} // Requires `OddIntegers` to be `StepBy<Range<u32>>` fn another_function() { let x: OddIntegers = (3..5).step_by(2); } } }
another_function
references OddIntegers
as the type of the variable x
-- but the value assigned to x
is also of type StepBy<Range<u32>>
, so everything works.
Each function within the defining scope must specify the same type
These two functions each independently require that OddIntegers
has the same underlying type. What is not ok is to have two functions that require different types:
#![allow(unused)] fn main() { type OddIntegers = impl Iterator<Item = u32>; // ^^^^^^^^^^^^^^^^^^^^^^^^^ :tada:! fn odd_integers(mut start: u32, stop: u32) -> OddIntegers { if (start % 2) == 0 { start += 1; } // Requires `OddIntegers` to be `StepBy<Range<u32>>` (start..stop).step_by(2) } fn another_function() { // Requires `OddIntegers` to be `Filter<...>`. // // ERROR! let x: OddIntegers = (3..5).filter(|x| x % 2 != 0); } }
Each function within the defining scope must specify the complete type
Similarly, it is not ok to have functions that only partially constrain and impl Trait. Consider this example:
#![allow(unused)] fn main() { type SomethingDebug = impl Debug; fn something_debug() -> SomethingDebug { // Constrains SomethingDebug to `Option<_>` // (where `_` represents "some type yet to be inferred") None } }
Here, the something_debug
function constrains SomethingDebug
to be some sort of Option
, but it doesn't say the full type. We only have Option<_>
. That's not good enough. You could change this function to specify what kind of option it is, though, and that would be fine:
#![allow(unused)] fn main() { type SomethingDebug = impl Debug; fn something_debug() -> SomethingDebug { // Constrains SomethingDebug to `Option<()>` None::<()> } }
Referencing the type alias outside of the module
Since it is declared to be public, the type alias OddIntegers
can be referenced from outside the module odd
. This allows callers to give a name to the return type of odd_integers
:
mod odd { pub type OddIntegers = impl Iterator<Item = u32>; pub fn odd_integers(mut start: u32, stop: u32) -> OddIntegers { /* as above */ } } fn main() { let odds: odd::OddIntegers = odd::odd_integers(1, 10); for i in odds { println!("{}", i); } }
Because that code is outside of the defining scope, however, it is not allowed to influence or observe the hidden type. It can only rely on the that were declared1 (e.g., Iterator<Item = u32>
). For example, the following code would not type check:
With the exception of auto traits.
mod odd { pub type OddIntegers = impl Iterator<Item = u32>; pub fn odd_integers(mut start: u32, stop: u32) -> OddIntegers { /* as above */ } } fn main() { // Error! let odds: std::iter::StepBy<std::iter::Range<u32>> = odd::odd_integers(1, 10); for i in odds { println!("{}", i); } }
This code fails because odds
is not known to have that exact type -- main
can only see that odds
returns "some kind of iterator".
Generic parameters and type alias impl trait
![nightly][] [planning rfc]: https://img.shields.io/badge/status-draft%20rfc-informational [accepted rfc]: https://img.shields.io/badge/status-accepted%20rfc-informational [needs decision]: https://img.shields.io/badge/status-needs%20decision-informational [stable]: https://img.shields.io/badge/status-stable-success [nightly]: https://img.shields.io/badge/status-nightly-important [may-change]: https://img.shields.io/badge/warning-implementation%20may%20change-critical
Type alias impl Traits can also be generic:
#![allow(unused)] fn main() { type SomeTupleIterator<I, J> = impl Iterator<Item = (I, J)>; }
The hidden type inferred for an impl trait that appears in a type alias may always reference any of the generic parameters from that type alias.
Limits on the generic arguments to a type alias impl trait
References to a type aliases that contain an impl trait, however, are subject to some restrictions. In particular, those type aliases can only be used with other generic types that are in scope, and each parameter must be distinct. Example:
#![allow(unused)] fn main() { fn foo1<A, B>() -> SomeTupleIterator<A, B> { /* ok */ } fn foo2<A, B>() -> SomeTupleIterator<B, A> { /* ok */ } fn foo3<A, B>() -> SomeTupleIterator<A, A> { /* not ok -- same parameter used twice */ } fn foo4<A, B>() -> SomeTupleIterator<A, u32> { /* not ok -- u32 is not a type parameter */ } }
These rules ensure that inference is tractable. Consider the case of -> SomeTupleIterator<A, u32>
. Imagine that foo4
returned an iterator over (A, u32)
tuples. How do we translate that value to the hidden type for SomeTupleIterator
? There would actually be multiple possibilities. Either it would be an iterator over (I, J)
(with I = A
and J = u32
in this case), or an iterator over (I, u32)
(with I = A
and J
unused).
Impl trait in return types
In the section on type aliases, we gave the example of a function odd_integers
that returned a type alias OddIntegers
. If you prefer, you can forego defining the type alias and simply return an impl Trait
directly:
#![allow(unused)] fn main() { fn odd_integers(start: u32, stop: u32) -> impl Iterator<Item = u32> { (start..stop).filter(|i| i % 2 != 0) } }
This is almost equivalent to the type alias we saw before, but there are two differences:
- The defining scope for the impl trait is just the function
odd_integers
, and not the enclosing module.- This means that other functions within the same module cannot observe or constrain the hidden type.
- There is no direct way to name the resulting type (because you didn't define a type alias).
- But see the section on naming impl trait in return type for indirect techniques.
Generic parameter capture
When you use an impl trait in return position, the hidden type may make use of any of the type parameters, and hence the following function is legal:
#![allow(unused)] fn main() { // Hidden type: Option<T>, which references T fn foo<T: Clone>(t: T) -> impl Clone { Some(t) } }
However, it may not reference lifetime parameters unless those lifetime parameters appear in the impl trait bounds. The following
#![allow(unused)] fn main() { // Error: hidden type `Option<&'a u32>` references `'a` fn foo<'a>(t: &'a u32) -> impl Clone { Some(t) } }
XXX document:
- you can capture type parameters
- but not lifetimes, unless they appear in the bounds
Naming impl trait in return types
Return position impl Trait introduces types that do not have proper names. If you find yourself frequently giving that type a name, your best bet is to introduce a type alias. However, in a pinch, it is possible to access those types by getting the type for the surrounding function and extracting its FnOnce::Output
associated type. Given a function like...
#![allow(unused)] fn main() { fn make_iter() -> impl Iterator<Item = u32> { 0 .. 100 } }
...one could use the type make_iter::Output
to access the inferred impl Iterator
(which will be a Range<u32>
, in this case).
Function types can generally be named via their path:
- For function items, simply the name of the function (
fn_name
) - For inherent methods, the fully qualified syntax of
Type::fn_name
- For trait methods defined in an impl, use the fully qualified syntax
<Type as Trait>::fn_name
(as iffn_name
were an associated type)
In each case, the type for the function is a zero-sized type that implements the Fn
, FnMut
, and FnOnce
traits. This type is considered to be defined in its surrounding module and it is also possible to use it in other contexts, such as to implement the Default
trait:
#![allow(unused)] fn main() { fn foo() -> u32 { 22 } impl Default for foo { fn default() -> Self { foo } } }
Return types in trait definitions and impls
When you use impl Trait
as the return type for a function within a trait definition or trait impl, the intent is the same: impls that implement this trait return "some type that implements Trait
", and users of the trait can only rely on that. However, the desugaring to achieve that effect looks somewhat different than other cases of impl trait in return position. This is because we cannot desugar to a type alias in the surrounding module; we need to desugar to an associated type (effectively, a type alias in the trait).
Consider the following trait:
#![allow(unused)] fn main() { trait IntoIntIterator { fn into_int_iter(self) -> impl Iterator<Item = u32>; } }
The semantics of this are analogous to introducing a new associated type within the surrounding trait;
#![allow(unused)] fn main() { trait IntoIntIterator { // desugared type IntoIntIter: Iterator<Item = u32>; fn into_int_iter(self) -> Self::IntoIntIter; } }
(In general, this associated type may be generic; it would contain whatever generic parameters are captured per the generic capture rules given previously.)
This associated type is introduced by the compiler and cannot be named by users.
The impl for a trait like IntoIntIterator
must also use impl Trait
in return position:
#![allow(unused)] fn main() { impl IntoIntIterator for Vec<u32> { fn into_int_iter(self) -> impl Iterator<Item = u32> { self.into_iter() } } }
This is equivalent to specify the value of the associated type as an impl Trait
:
#![allow(unused)] fn main() { impl IntoIntIterator for Vec<u32> { type IntoIntIter = impl Iterator<Item = u32> fn into_int_iter(self) -> Self::IntoIntIter { self.into_iter() } } }
Impl trait in let bindings
You can also use impl Trait
in the type of a local variable:
#![allow(unused)] fn main() { let x: impl Clone = 22_i32; }
This is equivalent to introducing a type alias impl trait with the scope of the enclosing function:
#![allow(unused)] fn main() { type X = impl Clone; let x: X = 22_i32; }
Auto traits and impl trait
When talking about output impl Traits in the previous section, we said that callers cannot rely on the precise hidden type, but must instead rely only on the declared bounds from the impl Trait. This was actually a simplification: in reality, callers are able to rely on some aspects of the hidden type. Specifically, they are able to deduce whether the hidden type implements the various auto traits, like Send
and Sync
:
#![allow(unused)] fn main() { fn odd_integers(start: u32, stop: u32) -> impl Iterator<Item = u32> { (start..stop).filter(|i| i % 2 != 0) } fn other_function() { let integers = odd_integers(); // This requires that `integers` is `Send`. // The compiler "peeks" at the hidden type // to check that this is true. std::thread::spawn(move || { for integer in integers { println!("{}", integer); } }).join(); } }
The motivation behind auto trait leakage is that it makes working with impl Trait
as convenient as other kinds of "newtype wrappers" when it comes to threading. For example, if you were to replace the odd_integers
return type with a newtype OddIntegers
that hides the iterator type (as is common practice)...
#![allow(unused)] fn main() { struct OddIntegers { iterator: Filter<Range<...>, ...> } fn odd_integers(start: u32, stop: u32) -> OddIntegers { ... } }
...you would find that OddIntegers
implements Send
if that hidden type (that appears in its private field) implements Send
. (In this particular case, it would be challenging to create a struct because it would have to refer to the returned closure type, which is anonymous.)
Appendix A: Inference details
Summary
The process to infer the hidden type for an impl Trait is as follows.
- When type-checking functions or code within the defining scope of an impl Trait
X
:- Accumulate subtyping constraints from type check:
- For example,
let var: X = 22_i32
would create the subtyping constraint thati32
is a subtype ofX
. - Similarly, given
fn foo() -> X
,let var: i32 = foo()
wold create the subtyping constraint thatX
be a subtype ofi32
.
- For example,
- The type
X
is assumed to implement the traits that appear in its bounds- Exception: auto traits. Proving an auto trait
X: Send
is done by "revealing" the hidden type and proving the auto trait against the hidden type.
- Exception: auto traits. Proving an auto trait
- Accumulate subtyping constraints from type check:
Terminology: Opaque type
An impl trait in output position is called an opaque type. We write the opaque type as O<P0...Pn>
, where P0...Pn
are the generic types on the opaque type. For a type alias impl trait, the generic types are the generics from the type alias. For a return position impl trait, the generics are defined by the RPIT capture rules.
Terminology: Opaque type bounds
Given an opaque type O<P1...Pn>
, let B1..Bn
be the bounds on that opaque type, appropriately substituted for P1..Pn
. For example, if the user wrote type Foo<T> = impl PartialEq<T> + Debug + 'static
and we have the opaque type Foo<u32>
, then the bounds would be PartialEq<u32>
, Debug
, and 'static
.
Notation: applying bounds to a type to create a where clause
Given a type T
and an opaque type bound B1
, we write T: B1
to signify the where clause that results from using T
as the "self type" for the bound B1
. For example, given the type i32
and the bound PartialEq<u32>
, the where clause would be i32: PartialEq<u32>
Given the type i32
and the bound 'static
, the where clause would be i32: 'static
.
Type checking opaque types
The following section describes how to type check functions or other pieces of code that reference opaque types.
Rules that always apply
The following rules for type-checking opaque types apply to all code, whether or not it is part of the type's defining scope.
Reflexive equality for opaque types
Like any type, an opaque type O<P0...Pn>
can be judged to be equal to itself. This does not require knowing the hidden type.
Example. The following program compiles because of this rule.
mod foo { type Foo = impl Clone; fn make_foo(x: u32) -> Foo { x } } fn main() { let mut x: Foo = make_foo(22); // Requires knowing that `Foo = Foo`. x = make_foo(44); }
Example. The following program does not compile even though the hidden types are the same.
mod foo { type Foo<T: Clone> = impl Clone; fn make_foo<A>(x: u32) -> Foo<A> { x } } fn main() { let mut x: Foo<String> = make_foo::<String>(22); // `Foo<String>` is not assumed to be equal to `Foo<char>`. x = make_foo::<char>(44); }
Method dispatch and trait matching
Given a method invocation o.m(...)
where the method receiver o
has opaque type O<P1...Pn>
, methods defined in the opaque type's bounds are considered "inherent methods", similar to a dyn
receiver or a generic type.
Similarly, the compiler can assume that O<P1...Pn>: Bi
is true for any of the declared bounds Bi
.
Example. The following program compiles because of this rule.
#![allow(unused)] fn main() { type Foo = impl Clone; fn clone_foo(f: Foo) -> Foo { // Clone can be called without knowing the hidden type: f.clone() } fn make_foo() -> Foo { // Hidden type constrained to be `u32` 22_u32 } }
Example. The following program does not compile even though the hidden type for Foo
is known within clone_foo
; this is because the type of f
is impl Clone
and we interpret that as a wish to only rely on the fact that Foo: Clone
and nothing else (modulo auto trait leakage, see next rule).
#![allow(unused)] fn main() { type Foo = impl Clone; fn clone_foo() { let f: Foo = 22_u32; // We don't know that `Foo: Debug`. debug!("{:?}", f); } }
Rules that only apply outside of the defining scope: auto trait leakage
The following type-checking rules can only be used when type-checking an item I that is not within the defining scope for the opaque type. When the type-checker for I wishes to employ one of these rules, it needs to "fetch" the hidden type for O
. This may create a cycle in the computation graph if determining the hidden type for O requires type-checking the body of I (e.g., to compute the hidden type for another opaque type O1); cyclic cases result in a compilation error.
Example. The following program compiles because of this rule:
#![allow(unused)] fn main() { type Foo = impl Clone; fn is_send<T: Send>() {} fn clone_foo() { let f: Foo = 22_u32; // We can leak the hidden type of `u32` defined within this function. is_send::<Foo>(); } }
Example. The following program does not compile because of a cycle:
#![allow(unused)] fn main() { fn is_send<T: Send>() { } mod foo { pub type Foo = impl Clone; pub fn make_foo() -> Foo { // Requires knowing hidden type of `Bar`: is_send::<crate::bar::Bar>(); 22_u32 } } mod bar { pub type Bar = impl Clone; pub fn make_bar() -> Foo { // Requires knowing hidden type of `Foo`: is_send::<crate::foo::Foo>(); 22_u32 } } }
Auto trait leakage
When proving O<P1...Pn>: AT
for some auto trait AT
, any item is permitted to use the hidden type H
for O<P1...Pn>
and show that H: AT
.
Rules that only apply within the defining scope: proposing an opaque type
The following type-checking rules can only be used when type-checking an item I
that is within the defining scope for the opaque type O
. If any of these rules are needed to type-check I
, then I
must compute a consistent type H
to use as the hidden type for O
. Any item I
that relies on the rules in this section is said to propose the hidden type H
for the opaque type O
. The process for inferring a hidden type is described below.
Equality between an opaque type and its hidden type
An opaque type O<P0...Pn>
can be judged to be equal to its hidden type H
.
Example. The following program compiles because of this rule:
#![allow(unused)] fn main() { type Foo = impl Clone; fn test() { // Requires that `Foo` be equal to the hidden type, `u32`. let x: Foo = 22_u32; } }
Auto trait leakage
When proving O<P1...Pn>: AT
for some auto trait AT
, I
can use the hidden type H
to show that H: AT
.
Example. The following program compiles because of this rule:
#![allow(unused)] fn main() { type Foo = impl Clone; fn is_send<T: Send>() {} fn test() { let x: Foo = 22_u32; is_send::<Foo>(); } }
Example. The following program does not compile. This is because it requires this rule to compile, but does not actually constrain the hidden type in any other way.
#![allow(unused)] fn main() { type Foo = impl Clone; fn is_send<T: Send>() {} fn test() { is_send::<Foo>(); } }
Determining the hidden type
To determine the hidden type for some opaque type O
, we examine the set C
of items that propose a hidden type O
. If that set is an empty set, there is a compilation error. If two items in the set propose distinct hidden types for O
, that is also an error. Otherwise, if all items propose the same hidden type H
for O
, then H
becomes the hidden type for O
.
Computing the hidden type proposed by an item I
Computing the hidden type H
for O<P1..Pn>
that is required by the item I
must be done independently from any other items within the defining scope of O
. It can be done by creating an inference variable ?V
for O<P1..Pn>
and unifying ?O
with all types that must be equal to O<P1...Pn>
. This inference variable ?V
is called the exemplar, as it is an "example" of what the hidden type is when P1..Pn
are substituted for the generic arguments of O
. Note that a given function may produce multiple exemplars for a single opaque type if it contains multiple references to O
with distinct generic arguments. Computing the actual hidden type is done by higher-order pattern unification.
Checking the hidden type proposed by an item I
Once the hidden type for an item I
is determined, we also have to check that the hidden type is well-formed in the context of the type alias and that its bounds are satisfied. Since the where clauses that were in scope when the exemplar was computed can be different from those declared on the opaque type, this is not a given.
Example. The following program does not compile because of this check. Here, the exemplar is T
and the hidden type is X
. However, the type alias does not declare that X: Clone
, so the impl Clone
bounds are not known to be satisfied.
#![allow(unused)] fn main() { type Foo<X> = impl Clone; fn make_foo<T: Clone>(t: T) { t } }
Limitations on exemplars
Whenever an item I
proposes a hidden type, the following conditions must be met:
- All of the exemplars from a given item
I
must map to the same hidden typeH
. - Each exemplar type
E
must be completely constrained and must not involve unbound inference variables.
Examples
The following function has two exemplars but they both map to the same hidden type, so it is legal:
#![allow(unused)] fn main() { type Foo<T, U> = impl Debug; fn compatible<A, B>(a: A, b: B) { // Exemplar: (A, B) for Foo<A, B> // Hidden type: (T, U) let _: Foo<A, B> = (a, b); // Exemplar: (B, A) for Foo<B, A> // Hidden type: (T, U) let _: Foo<B, A> = (b, a); } }
The following program is illegal because of the restriction against having incompatible examplars.
#![allow(unused)] fn main() { type Foo<T, U> = impl Debug; fn incompatible<A, B>(a: A, b: B) { // Exemplar: (A, B) // Hidden type: (T, U) let _: Foo<A, B> = (a, b); // Exemplar: (B, A) // Hidden type: (U, T) let _: Foo<A, B> = (b, a); } }
The following program is illegal because of the restriction against having incomplete types. This is true even though the two functions, taken in aggregate, have enough information to compute the hidden type for Foo
.
#![allow(unused)] fn main() { type Foo = impl Debug; fn incomplete1() { // Computes an incomplete exemplar of `Result<(), _>`. let x: Foo = Ok(()); } fn incomplete2() { // Computes an incomplete exemplar of `Result<_, ()>`. let x: Foo = Err(()); } }
Higher-order pattern unification
When an item I
finishes its type check, it will have computed an exemplar type E
for the opaque type O<P1..Pn>
. This must be mapped back to the hidden type H
. Thanks to the limitations on type alias generic parameters, this can be done by a process called higher-order pattern unification (a limited, tractable form of higher-order unification). Higher-order pattern unification is best explained by example.
Consider an opaque type Foo
:
#![allow(unused)] fn main() { type Foo<T: Clone, U: Clone> = impl Clone; }
and an item make_foo
that contains Foo
:
#![allow(unused)] fn main() { fn make_foo<X: Clone, Y: Clone>(x: X, y: Y) -> Foo<X, Y> { vec![(x, y)] } }
Here the opaque type O<P1, P2>
is Foo<X, Y>
(i.e., P1
is X
and P2
is Y
) and the exemplar type E
is Vec<(X, Y)>
. We wish to compute the hidden type, which will be Vec<(T, U)>
-- note that the hidden type references the generics from the declaration of Foo
, whereas the exemplar references the generics from make_foo
. Computing the hidden type can be done by finding each occurence of a generic argument Pi
(in this case, X
and Y
) and mapping to the corresponding generic parameter i
from the opaque type declaration (in this case, T
and U
respectively).
This process is well-defined because of the limitations on type alias generic parameters. Thanks to those limitations, we know that:
- each generic argument
Pi
will be some generic type onmake_foo
; - each generic argument
Pi
to the opaque type will be distinct from each other argumentPj
.
In particular, these limitations mean that whenever we see an instance of some parameter Pi
in the exemplar, the only way that Pi
could appear in the final hidden type is if it was introduced by substitution for one of the generic arguments.
To see the ambiguity that is introduced without these limitations, consider what would mappen if you had a function like bad_make_foo1
:
#![allow(unused)] fn main() { fn bad_make_foo1() -> Foo<u32, i32> { vec![(22_u32, 22_i32)] } }
Here the exemplar type is Vec<(u32, i32)>
, but the hidden type is ambiguous. It could be Vec<(T, U)>
, as before, but it could also just be Vec<(u32, i32)>
. The problem is that this example violates the first restriction: the parameters u32
and i32
are not generics on bad_make_foo1
. This means that they are types which can be named from both the definition of type Foo
and from within the scope of bad_make_foo1
, introducing ambiguity.
A similar problem happens when type parameters are repeated, as illustrated by bad_make_foo2
:
#![allow(unused)] fn main() { fn bad_make_foo2<X: Clone>(x: X) -> Foo<X, X> { vec![(x.clone(), x.clone())] } }
The exemplar type here is Vec<(X, X)>
. The hidden type, however, could either be Vec<(T, T)>
or Vec<(T, U)>
or Vec<(U, U)>
. All of them would be the same after substitution.
Appendix B: Where can impl trait be used
Overview
Impl trait is accepted in the following locations:
Position | Who determines the hidden type | Role | Status |
---|---|---|---|
Argument position | Each caller | Input | |
Type alias | Code within the enclosing module | Output | |
Return position, free fns | The function body | Output | |
Return position, inherent impls | The function body | Output | |
Return position, trait impls | The function body | Output | |
Return position, traits | The impl | Input | |
Let binding | The enclosing function or code block | Output | |
Const binding | The const initializer | Output | |
Static binding | The static initializer | Output |
Impl trait is not accepted in other locations; Appendix C covers some of the locations where impl Trait
is not presently accepted and why.
Role: Input vs output
Impl traits in general play one of two roles.
Input impl trait
An input impl trait corresponds loosely to a generic parameter. The code that references the impl Trait may be instantiated multiple times with different values. For example, a function using impl Trait in argument position can be called with many different types for the impl Trait.
Output impl trait
An output impl trait plays a role similar to a type that is given by the user. In this case, the impl trait represents a single type (although that type may be relative to generic types are in scope) which is inferred from the code around the definition. For example, a free function with an impl Trait in return position will have the true return type inferred from the function body. The type represented by an output impl trait is called the hidden type. The code that is used to infer the value for an output impl trait is called its defining scope.
General rules for "input" vs "output"
In general, the role of impl Trait
and '_
both follow the same rules in terms of being "input vs output". When in an argument listing, that is an "input" role and they correspond to a fresh parameter in the innermost binder. Otherwise, they are in "output" role and the corresponding to something which is inferred or selected from context (in the case of '_
in return position, it is selected based on the rules of lifetime elision; '_
within a function body corresponds to inference).
Type alias impl trait
#![allow(unused)] fn main() { type Foo = impl Trait; }
Creates a opaque type whose value will be inferred by the contents of the enclosing module (and its submodules).
Fn argument position
#![allow(unused)] fn main() { fn foo(x: impl Trait) }
becomes an "anonymous" generic parameter, analogous to
#![allow(unused)] fn main() { fn foo<T: Trait>(x: T) }
However, when impl Trait
is used on a function, the resulting type parameter cannot be specified using "turbofish" form; its value must be inferred. (status: this detail not yet decided).
Places this can be used:
- Top-level functions and inherent methods
- Trait methods
- Implication: trait is not dyn safe
Fn return position
#![allow(unused)] fn main() { fn foo() -> impl Trait) }
becomes an "anonymous" generic parameter, analogous to
#![allow(unused)] fn main() { fn foo<T: Trait>(x: T) type Foo = impl Trait; // defining scope: just the fn fn foo() -> Foo }
Places this can be used:
- Top-level functions and inherent methods
- Trait methods (pending; no RFC)
Let bindings
#![allow(unused)] fn main() { fn foo() { let x: impl Debug = ...; } }
becomes a type alias impl trait, analogous to
#![allow(unused)] fn main() { type Foo = impl Debug; // scope: fn body fn foo() { let x: Foo = ...; } }
Appendix C: Where can impl trait NOT be used
Where is impl Trait not (yet?) accepted and why.
dyn Types, angle brackets
#![allow(unused)] fn main() { fn foo(x: &mut dyn Iterator<Item = impl Debug>) }
dyn Types, parentheses
#![allow(unused)] fn main() { fn foo(x: &mut dyn FnMut(impl Debug)) }
Unclear whether this should (eventually) be short for dyn for<T: Debug> FnMut(T)
(which would not be legal) to stay analogous to impl FnOnce(impl Debug)
.
dyn Types, return types
#![allow(unused)] fn main() { fn foo(x: &mut dyn FnMut() -> impl Debug) }
Nested impl trait, parentheses
#![allow(unused)] fn main() { fn foo(x: impl FnMut(impl Debug)) }
Unclear whether this should (eventually) be short for impl for<T: Debug> FnMut(T)
or some other notation.
Where clauses, angle brackets
#![allow(unused)] fn main() { fn foo() where T: PartialEq<impl Clone> }
Where clauses, parentheses
#![allow(unused)] fn main() { fn foo() where T: FnOnce(impl Clone) }
Where clauses, return position
#![allow(unused)] fn main() { fn foo() where T: FnOnce() -> impl Clone }
Struct fields
#![allow(unused)] fn main() { struct Foo { x: impl Debug } }
It would be plausible to accept this as equivalent to a type alias at module scope, which seems reasonable, but it is easy to write that explicitly, and there may be future meanings that are more interesting (such as an existential type local to the struct).
It is not possible to accept this in input position, because that would imply that the type Foo
has more generic parameters than are written on the type itself. Unlike with functions, where the values of those generic parameters can be inferred from the arguments, structs can appear in many contexts where inferring the values of those generic types would not be tractable.
Appendix D: Glossary
See the list of terms in the sidebar.
Defining scope
The defining scope of an impl Trait is the code that is used to infer its hidden type. See the explanation of type alias impl trait for more details.
Hidden type
The hidden type of an impl Trait is the underlying type that the impl Trait
represents. The compiler always infers the hidden type for an impl trait from the rest of the code. In some cases, there is just a single hidden type, but in other cases -- notably argument position impl trait -- there can be multiple hidden types (e.g., one per call site).
Input impl trait
An impl Trait that can be instantiated multiple times with different values, similar to a generic parameter. See Appendix B for more details.
Output impl trait
An impl Trait that corresponds to a single, hidden type that is inferred from the surrounding code. See Appendix B for more details.
✨ RFCs
The design for impl trait has been defined by a number of RFCs and decisions. This repository represents the "accumulated state" of all these RFCs, and also includes drafts of new RFCs that are in the works. Sometimes, though, it is useful to return to an RFC to see the details. Here is a comprehensive listing of the impl trait RFCs along with some of the core ideas they describe. Please keep in mind that details of the implementation or design may have been amended since the RFC was accepted, however.
Accepted RFCs
- RFC 1522: Proposed
-> impl Trait
notation in return position for free functions and inherent methods, inclued how auto traits "leak" through impl Trait. - RFC 1951: Proposed argument position impl trait with a uniform syntax for input vs output role. Defined the capture rules regarding which generic parameters are in scope for return position impl trait.
- RFC 2071: Proposed "type alias impl trait" and defined the core idea of a hidden type that is inferred from multiple functions. Established the principle that each of those functions should independently define the full hidden type. Also introduced the ability to have
impl Trait
in let, const, and static bindings. The syntax at the time wasexistential type
, later revised by RFC 2515. - RFC 2515: Revised the syntax from
existential type Foo: Trait
totype Foo = impl Trait
.
Draft RFCs
- Return position impl trait in traits: Extends return position impl Trait to be usable in traits and trait impls. (explainer)
- Named function types: Introduces a way to name function types, and hence their return types, which in turn permits "return position impl trait" to be named.
Return position impl Trait in traits
This is a draft RFC that will be submitted to the rust-lang/rfcs repository when it is ready.
Feedback welcome!
Return position impl Trait in traits
- Feature Name: (fill me in with a unique ident,
my_awesome_feature
) - Start Date: (fill me in with today's date, YYYY-MM-DD)
- RFC PR: rust-lang/rfcs#0000
- Rust Issue: rust-lang/rust#0000
- Developed as part of the impl traits lang team initiative
Summary
- Permit
impl Trait
in fn return position within traits and trait impls. - This desugars to an anonymous associated type.
Motivation
The impl Trait
syntax is currently accepted in a variety of places within the Rust language to mean "some type that implements Trait
" (for an overview, see the explainer from the impl trait initiative). For function arguments, impl Trait
is equivalent to a generic parameter and it is accepted in all kinds of functions (free functions, inherent impls, traits, and trait impls). In return position, impl Trait
corresponds to an opaque type whose value is inferred. In that role, it is currently accepted only in free functions and inherent impls. This RFC extends the support to cover traits and trait impls, just like argument position.
Example use case
The use case for -> impl Trait
in trait fns is similar to its use in other contexts: traits often wish to return "some type" without specifying the exact type. As a simple example that we will use through the RFC, consider the NewIntoIterator
trait, which is a variant of the existing IntoIterator
that uses impl Iterator
as the return type:
#![allow(unused)] fn main() { trait NewIntoIterator { type Item; fn into_iter(self) -> impl Iterator<Item = Self::Item>; } }
Guide-level explanation
This section assumes familiarity with the basic semantics of impl trait in return position.
When you use impl Trait
as the return type for a function within a trait definition or trait impl, the intent is the same: impls that implement this trait return "some type that implements Trait
", and users of the trait can only rely on that. However, the desugaring to achieve that effect looks somewhat different than other cases of impl trait in return position. This is because we cannot desugar to a type alias in the surrounding module; we need to desugar to an associated type (effectively, a type alias in the trait).
Consider the following trait:
#![allow(unused)] fn main() { trait IntoIntIterator { fn into_int_iter(self) -> impl Iterator<Item = u32>; } }
The semantics of this are analogous to introducing a new associated type within the surrounding trait;
#![allow(unused)] fn main() { trait IntoIntIterator { // desugared type IntoIntIter: Iterator<Item = u32>; fn into_int_iter(self) -> Self::IntoIntIter; } }
(In general, this associated type may be generic; it would contain whatever generic parameters are captured per the generic capture rules given previously.)
This associated type is introduced by the compiler and cannot be named by users.
The impl for a trait like IntoIntIterator
must also use impl Trait
in return position:
#![allow(unused)] fn main() { impl IntoIntIterator for Vec<u32> { fn into_int_iter(self) -> impl Iterator<Item = u32> { self.into_iter() } } }
This is equivalent to specify the value of the associated type as an impl Trait
:
#![allow(unused)] fn main() { impl IntoIntIterator for Vec<u32> { type IntoIntIter = impl Iterator<Item = u32> fn into_int_iter(self) -> Self::IntoIntIter { self.into_iter() } } }
Reference-level explanation
Equivalent desugaring for traits
Each -> impl Trait
notation appearing in a trait fn return type is desugared to an anonymous associated type; the name of this type is a fresh name that cannot be typed by Rust programmers. In this RFC, we will use the name $
when illustrating desugarings and the like.
As a simple example, consider the following (more complex examples follow):
#![allow(unused)] fn main() { trait NewIntoIterator { type Item; fn into_iter(self) -> impl Iterator<Item = Self::Item>; } // becomes trait NewIntoIterator { type Item; type $: Iterator<Item = Self::Item>; fn into_iter(self) -> <Self as NewIntoIterator>::$; } }
Equivalent desugaring for trait impls
Each -> impl Trait
notation appearing in a trait impl fn return type is desugared to the same anonymous associated type $
defined in the trait along with a function that returns it. The value of this associated type $
is an impl Trait
.
#![allow(unused)] fn main() { impl NewIntoIterator for Vec<u32> { type Item = u32; fn into_iter(self) -> impl Iterator<Item = Self::Item> { self.into_iter() } } // becomes impl NewIntoIterator for Vec<u32> { type Item = u32; type $ = impl Iterator<Item = Self::Item>; fn into_iter(self) -> <Self as NewIntoIterator>::$ { self.into_iter() } } }
Impl trait must be used in both trait and trait impls
Using -> impl Trait
notation in a trait requires that all trait impls also use -> impl Trait
notation in their return types. Similarly, using -> impl Trait
notation in an impl is only legal if the trait also uses that notation:
#![allow(unused)] fn main() { trait NewIntoIterator { type Item; fn into_iter(self) -> impl Iterator<Item = Self::Item>; } // OK: impl NewIntoIterator for Vec<u32> { type Item = u32; fn into_iter(self) -> impl Iterator<Item = u32> { self.into_iter() } } // Not OK: impl NewIntoIterator for Vec<u32> { type Item = u32; fn into_iter(self) -> vec::IntoIter<u32> { self.into_iter() } } }
Rationale: Maximizing forwards compatibility. We may wish at some point to permit impls and traits to diverge but there is no reason to do it at this time.
Generic parameter capture and GATs
As with -> impl Trait
in other kinds of functions, the hidden type for -> impl Trait
in a trait may reference any of the type or const parameters declared on the impl or the method; it may also reference any lifetime parameters that explicitly appear in the trait bounds (details). We say that a generic parameter is captured if it may appear in the hidden type.
When desugaring, captured parameters from the method are reflected as generic parameters on the $
associated type. Furthermore, the $
associated type has the required brings whatever where clauses are declared on the method into scope (excepting those which reference other parameters that are not captured). This transformation is precisely the same as the one which is applied to other forms of -> impl Trait
, except that it applies to an associated type and not a top-level type alias.
Example:
#![allow(unused)] fn main() { trait RefIterator for Vec<u32> { type Item<'me> where Self: 'me; fn iter<'a>(&'a self) -> impl Iterator<Item = Self:Item<'a>>; } // Since 'a is named in the bounds, it is captured. // `RefIterator` thus becomes: trait RefIterator for Vec<u32> { type Item<'me> where Self: 'me; type $<'a>: impl Iterator<Item = Self::Item<'a>> where Self: 'a; // Implied bound from fn fn iter<'a>(&'a self) -> Self::$<'a>; } }
Dyn safety
To start, traits that use -> impl Trait
will not be considered dyn safe, even if the method has a where Self: Sized
bound. This is because dyn types currently require that all associated types are named, and the $
type cannot be named. The other reason is that the value of impl Trait
is often a type that is unique to a specific impl, so even if the $
type could be named, specifying its value would defeat the purpose of the dyn
type, since it would effectively identify the dynamic type.
Rationale and alternatives
Can traits migrate from a named associated type to impl Trait
?
Not compatibly, no, because they would no longer have a named associated type.
Can traits migrate from impl Trait
to a named associated type?
Generally yes, but all impls would have to be rewritten.
Would there be any way to make it possible to migrate from impl Trait
to a named associated type compatibly?
Potentially! There have been proposals to allow the values of associated types that appear in function return types to be inferred from the function declaration. So the trait has fn method(&self) -> Self::Iter
and the impl has fn method(&self) -> impl Iterator
, then the impl would also be inferred to have type Iter = impl Iterator
(and the return type rewritten to reference it). This may be a good idea, but it is not proposed as part of this RFC.
What about using a named associated type?
One alternative under consideration was to use a named associated type instead of the anonymous $
type. The name could be derived by converting "snake case" methods to "camel case", for example. This has the advantage that users of the trait can refer to the return type by name.
We decided against this proposal:
- Introducing a name by converting to camel-case feels surprising and inelegant.
- Return position impl Trait in other kinds of functions doesn't introduce any sort of name for the return type, so it is not analogous.
There is a need to introduce a mechanism for naming the return type for functions that use -> impl Trait
; we plan to introduce a second RFC addressing this need uniformly across all kinds of functions.
As a backwards compatibility note, named associated types could likely be introduced later, although there is always the possibility of users having introduced associated types with the same name.
Does auto trait leakage still occur for -> impl Trait
in traits?
Yes, so long as the compiler has enough type information to figure out which impl you are using. In other words, given a trait function SomeTrait::foo
, if you invoke a function <T as SomeTrait>::foo()
where the self type is some generic parameter T
, then the compiler doesn't really know what impl is being used, so no auto trait leakage can occur. But if you were to invoke <u32 as SomeTrait>::foo()
, then the compiler could resolve to a specific impl, and hence a specific impl trait type alias, and auto trait leakage would occur as normal.
Would introducing a named associated type be a breaking change for a trait?
Converting from returning impl trait to an explicit associated type is a breaking change for impls of the trait. Given this code:
#![allow(unused)] fn main() { trait Foo { fn bar() -> impl Display; } impl Foo for u32 { fn bar() -> impl Display { self } } }
transforming it to the following:
#![allow(unused)] fn main() { trait Foo { type Bar: Display; fn bar() -> Self::Bar; } impl Foo for u32 { fn bar() -> impl Display { self } } }
Results in an impl in the impl Foo for u32
. The Future possibilities section discusses some possible ways we could mitigate this in the future by other language extensions.
Prior art
There are a number of crates that do desugaring like this manually or with procedural macros. One notable example is real-async-trait.
Unresolved questions
- None.
Future possibilities
We expect to introduce a mechanism for naming the result of -> impl Trait
return types in a follow-up RFC (see the draft named function types rfc for the current thinking).
Similarly, we expect to be introducing language extensions to address the inability to use -> impl Trait
types with dynamic dispatch. These mechanisms are needed for async fn as well. A good writeup of the challenges can be found on the "challenges" page of the async fundamentals initiative.
Finally, it would be possible to introduce a mechanism that allows users to give a name to the associated type that is returned by impl trait. One proposed mechanim is to support an inference mechanism, so that one if you have a function fn foo() -> Self::Foo
that returns an associated type, the impl only needs to implement the function, and the compiler infers the value of Foo
from the return type. Another options would be to extend the impl trait syntax generally to let uses give a name to the type alias or parameter that is introduced.
Draft RFC: Named function types
This is a draft RFC that will be submitted to the rust-lang/rfcs repository when it is ready.
Feedback welcome!
- Feature Name: (fill me in with a unique ident,
my_awesome_feature
) - Start Date: (fill me in with today's date, YYYY-MM-DD)
- RFC PR: rust-lang/rfcs#0000
- Rust Issue: rust-lang/rust#0000
Summary
- Given a function
fn foo
, introduce a type namedfn#foo
to represent the zero-sized type of the function ("function def").- When possible, a type named
foo
is also introduced, unless another type with that name already exists.
- When possible, a type named
- The generic parameters of a "function def" type are defined to include only named parameters. Anonymous parameters that appear in the argument are excluded:
- Elided lifetimes in argument position are excluded.
impl Trait
in argument position are excluded.- This implies that turbofish with a "function def" type cannot name the
impl Trait
arguments. - As a temporary measure, "function def" types that have named, late-bound lifetimes cannot accept lifetime parameters.
- This rule already exists today.
Motivation
Problems we are solving
This RFC proposes two related changes that, together, work to close two remaining major "holes" in the impl Trait
story:
- How to name the return type of a function
- How turbofish interacts with
impl Trait
in argument position (explainer)
These two problems at first seem orthogonal, but they turn out to be related. Let's first introduce the two problems.
Naming the return type of a function
"Return position" impl Trait (RPIT) generally refers to an "anonymous" opaque type whose value is inferred by the compiler:
#![allow(unused)] fn main() { fn odd_integers(start: u32, stop: u32) -> impl Iterator<Item = u32> { (start..stop).filter(|i| i % 2 == 0) } // becomes something like: type OddIntegers = impl Iterator<Item = u32>; fn odd_integers(start: u32, stop: u32) -> OddIntegers { (start..stop).filter(|i| i % 2 == 0) } }
When using RPIT in traits, this anonymous type is a kind of associated type:
#![allow(unused)] fn main() { trait IntoIntIterator { fn into_int_iter(self) -> impl Iterator<Item = u32>; } // becomes trait IntoIntIterator { // desugared type IntoIntIter: Iterator<Item = u32>; fn into_int_iter(self) -> Self::IntoIntIter; } }
In both cases, it would sometimes be nice to be able to name that return type! Of course, people can introduce type aliases or associated types (similar to the desugared form), but that is inconvenient, and it requires that the API author has made sure to do so.
Turbofish and impl Trait in argument position
impl Trait
in argument position introduces a new generic parameter to the function:
#![allow(unused)] fn main() { fn collect_to_vec<T>(iter: impl Iterator<Item = T>) -> Vec<T> { iter.collect() } // is roughly equivalent to: fn collect_to_vec_desugared<T, I>(iter: I) -> Vec<T> where I: Iterator<Item = T>, { iter.collect() } }
With the desugared version, users can write an expression that manually specifies the values for its type parameters:
#![allow(unused)] fn main() { let f = collect_to_vec_desugared::<u32, vec::IntoIter<u32>>; }
The question addressed here is whether it should be possible to write an equivalent expression for collect_to_vec
.
Background material
This RFC covers some esoteric corners of Rust's type system. This section provides background material that introduces terms that are necessary to understand what follows.
Function definition types
Currently, when you define a function my_function
, this declares a value my_function
that can be accessed by the user. The type of that value, however, is a bit complicated. Consider this program:
fn my_function() { println!("Hello, world"); } fn main() { let f = my_function; println!("{}", std::mem::size_of_val(&f)); let g: fn() = my_function; println!("{}", std::mem::size_of_val(&f)); }
Here we reference my_function
twice, once to create a variable named f
with an inferred type, and once to declare a variable g
with type fn()
. You might expect that the type of f
would be inferred to fn()
and that these two variables are equivalent, but if you run this program you will find that their sizes are different: 0 and 8 respectively (playground). What is going on?
The answer is that the type of my_function
is actually a special zero-sized type called a function definition type (hereafter: fndef). There is a unique fndef type for every function in your program. These types implement the Fn
traits, so they can be called: when you call one, the compiler knows exactly what function was called just from the type, since it is tied to a specific function. In contrast, the fn()
type represents a function pointer -- it too can be called, but, without some kind of dataflow analysis, the compiler doesn't know what function is being called. This is analogous to using traits with generic types vs dyn Trait
.
There is currently no syntax for a fndef type, so there is no way to write the type of f
above. The closest you can do is an as-yet-unimplemented extension impl Trait
, impl trait in let bindings, which would let you write let f: impl Fn() = my_function
.
Generic function definition types
The fndef type for a generic function is itself generic. You can specify the values for those generics using turbofish:
fn generic_fn<T>(t: T) { } fn main() { let f = generic_fn::<u32>; }
Early vs late binding
Rust groups generic parameters on functions into two categories: early vs late bound. Early bound parameters are those whose values must be supplied when the function is referenced (either explicitly via turbofish, or implicitly with inference). Late-bound parameters are parameters that are bound only at the time when the function is called. At present, generic type and const parameters are always early bound; lifetime parameters can be either early or late depending on whether they are referenced outside of the argument listing (the exact rules are not pertinent to this RFC).
It is perhaps easiest to understand this by considering the impl of the FnOnce
trait for a fndef type. Consider this example, of a function create_with_default
that will create an instance of C
. The input supplied to the creation function is either the string input
or -- in some cases -- a default string.
#![allow(unused)] fn main() { trait Create { fn create(input: &str); } fn create_with_default<'a, C: Create>(input: &'a str) -> C { if input.is_empty() { C::create("default") } else { C::create(input) } } }
We will refer to the fndef type of create_with_default
as create_with_default
1. Conceptually, there is an impl of FnOnce
for create_with_default
rather like so:
#![allow(unused)] fn main() { impl<'a, C> FnOnce<(&'a str,)> for create_with_default<C> { // ^^^^^^^^^^ ^^^ // | | // | Early bound type parameters // | appear in this list. // | // Late bound parameters appear only in these // argument types. type Output = C; fn call(self, args: (&'a str,)) { (self)(args.0) } } }
Here there are two "input types" supplied to the trait: the argument parameter A
to FnOnce<A>
, which defines the types of the arguments being supplied, and the self type (create_with_default<C>
), which defines the type of the value being called. The set of generic parameters on the impl is the same as the set on the fn
declaration. We can classify those parameters in two ways:
- Late-bound parameters are those that appear only in the arguments -- this means that they different values can be supplied for them each time the function is called. In this example, the lifetime
'a
is late-bound. - Early-bound parameters are those that appear in the
Self
type. This means that they are "baked into" the function that is being called (which always has a single type) and cannot change from call to call. In this example,C
is early-bound.
In general, it is better for parameters to be late-bound, as that allows the user more flexibility. However, because of the rules that every impl must be constrained2, parameters that don't appear in the argument types must appear in the Self type, and hence be early bound. In practice, the compiler today only allows lifetimes to be late-bound; all type parameters are early bound. This desugaring however shows that it is possible to consider type parameters as late-bound as well.
...which are there to ensure that the compiler can always figure out what impl to invoke at monomorphization time.
Surprise! This is precisely the notation that this RFC is going to propose!
Guide-level explanation
Please refer to the impl trait explainer, which has a section on names for return position impl trait that aims to be a "end-user manual" for this feature.
Reference-level explanation
The proposal has a few parts:
- Named fndef types:
- For every function
my_function
, define both a value and a type, except when there is already a type with that name. - As a disambiguator, also export a type
fn#my_function
that can be used to unambiguously name the fndef type. - In traits, for every
fn my_function
item, introduce atype my_function
associated type (except when there is already an associated type with that name).
- For every function
- Late-bound argument position impl Trait:
- Change
impl Trait
in argument position so that it desugars to "late-bound types" - In so doing, settle the question of whether
impl Trait
in argument position can be constrained with turbofish (no)
- Change
- Random bug fixes:
- Inherent associated types (
impl Foo { type Bar = u32; }
and you can writeFoo::Bar
). - Fix the
Foo::Output
code in the compiler (not really RFC worthy).
- Inherent associated types (
Named fndef types
Introduce a set of changes designed to make it so that users can name the fndef type for every function by writing types whose name is the same as the name that refers to the fn by value:
foo
to name the type for a top-level fnfoo
.Type::foo
to name the type for an inherent methodfoo
.<T as Trait>::foo
to name the type for a trait methodfoo
(orTrait::foo
and so forth).
Top-level function items
Top-level function declarations like fn my_function
will also export a value in the type namespace with the same name (my_function
) that corresponds to the fndef type for that function. Therefore, the following works:
fn foo() {} struct SomeType { f: foo // refers to the fndef type for `foo` } fn main() { let x = Sometype { f: foo }; }
Function definitions in traits
Function declarations in inherent and trait impls introduce an associated type into the surrounding impl with the same name as the function whose value is the fndef type for the function:
#![allow(unused)] fn main() { trait Clone { // Introduces the equivalent of: // // type clone; fn clone(&self); } }
Function definitions in impls
Function declarations in inherent and trait impls introduce an associated type into the surrounding impl with the same name as the function whose value is the fndef type for the function:
#![allow(unused)] fn main() { struct MyType { } impl MyType { // Introduces: // // type new = <fndef for new>; fn new() -> Self { } } impl Clone for MyType { // Introduces: // // type clone = <fndef for clone>; fn clone(&self) -> ... { } } }
Shadowing rules
Although Rust's naming conventions make this unlikely, it is possible today to define a type and a function with the same name. This obviously presents a conflict in attempting to introduce types with the same names as functions. This section discusses the possible shadowing that can arise in each case and how we manage it. The general strategy is to only generate the fn type if there is not already a type defined with the same name.
Edition Rust.next
The plan is that in the edition Rust.next (presumably Rust 2024), all cases of shadowing will become hard errors. In the current edition, however, cases of shadowing yield warnings where needed to maintain backwards compatibility.
Traits and trait impls
If a trait defines an explicit associated type with the same name as one of its functions, then the associated type for that function is suppressed. This also applies to associated types defined in supertraits. Therefore:
#![allow(unused)] fn main() { trait Foo: Bar { // A `foo` associated type exists: // // nothing is generated for the `fn foo`. type foo; fn foo(&self); // A `bar` associated type exists in the supertrait: // // nothing is generated for the `fn bar`. fn bar(&self); // The `Bar` supertrait defines an associated type `baz`, but implicitly: // // generate an associated type `baz.` fn baz(&self); } trait Bar { type bar; fn baz(&self); } }
In all of these cases, we issue warnings. In Rust 2024, this will be an error.
Rationale: Currently given T: Foo
, T::bar
would be legal today, so we don't want to generate a type there. But otherwise we do.
Inherent impls
Inherent impls do not support associated types today, so we don't have to worry about conflicts.
If an associated type defined on T has the same name as an inherent function on T, then we report an error.
Tuple struct and enum variants
Although not a function declaration, tuple structs and enum variants define constructor functions with the same name as the type itself:
#![allow(unused)] fn main() { struct Foo(u32); // defines a fn `Foo` of type `u32 -> Foo` }
In this case, we simply do not generate a fndef type for the constructor. (This is also true in Rust 2024.)
Top-level functions
Top-level functions can conflict with named types in a number of ways (examples follow). In all such cases, the intent is to have the function not generate a type alias.
Conflict with prelude (playground)
fn Vec() { } fn main() { let x = Vec(); }
Conflict with name from use statement (playground)
mod foo { pub type bar = u32; } use foo::bar; fn bar() { } fn main() { }
Conflict with macro-generated items (playground)
mod foo { macro_rules! make_bar { () => { pub type bar = u32; } } make_bar!(); } use foo::bar; fn bar() { } fn main() { let x: bar = 22_u32; }
Late-bound argument position impl Trait and turbofish
Currently, impl Trait in argument position is desugared to the equivalent of another explicit parameter:
#![allow(unused)] fn main() { fn foo(x: impl Debug) { } // becomes roughly equivalent to // fn foo<D: Debug>(x: D) { } }
This parameter is currently considered "early-bound". As discussed in the motivation, that means that the fndef type foo<D>
is generic over some D: Debug
. Conceptually it looks something like this:
#![allow(unused)] fn main() { struct foo<D: Debug>; impl<D: Debug> FnOnce<(D,)> for foo<D> { ... } }
This means that if you were to try and name the type of foo
, say in a struct, you would need some to way to specify the value of D
:
#![allow(unused)] fn main() { struct Wrapper { f: foo // What is the value of D here? } }
We could allow you to write foo<u32>
, but that would be strange, because if you look at the definition of foo
, there are no declared type parameters. What's more, if there were multiple impl Trait
, we'd have to define an arbitrary ordering for them. Overall, we'd prefer for people to be able to think of impl Trait
more intuitively without having to understand the desugaring.
As noted in that section, however, generic parameters that appear in the argument types can be made late-bound instead of early-bound. In the case of impl Trait
arguments, they always (by definition) appear in the argument types. Therefore, we could make them "late-bound", so that they are only parameters of the impl and not of the type. Conceptually then the fndef type for foo
would be like this:
#![allow(unused)] fn main() { struct foo; impl<D: Debug> FnOnce<(D,)> for foo { ... } }
As a result, the type of foo
is just written as foo
, with no type arguments, and hence this struct is perfectly legal:
#![allow(unused)] fn main() { struct Wrapper { f: foo } }
What's more, making impl Trait
late bound is actually more flexible. For example, this code does not compile today, but it would under this proposal:
fn foo(d: impl Debug) { /* ... */ } fn main() { let f = foo; f(22_u32); // call once with `u32` f(22_i32); // call again with `i32` }
Turbofish interaction
This also settles 'en passante' on of the open questions about impl Trait in argument position: should you be allowed to specify their value in turbofish? Clearly, the answer under this proposal is no, as there are no type parameters whose value needs to be specified.
Implication: migration to impl Trait
is not fully backwards compatible
Settling the turbofish question in this matter does mean that one cannot migrate from a generic function to impl Trait
with perfect fidelity:
#![allow(unused)] fn main() { fn foo<D: Debug>(d: D) { } }
is different than
#![allow(unused)] fn main() { fn foo(d: impl Debug) { } }
because the former permits turbofish and the latter does not.
Implication: Backwards incompatibility around inference
Unfortunately, there is a slight (largely theoretical) backwards incompatibility with making impl Trait
type parameters late bound. It is possible today to leverage inference across calls to the same function. The following code compiles today but would become an error in the future:
fn foo(d: impl Debug) { /* ... */ } fn main() { let f = foo; f(None); // call with `Option<_>`, unknown value type f(Some(22_i32)); // call again with `Option<i32>` }
Today, we are able to infer that both calls must be using an Option<i32>
. This works because all calls to f
must use the same value for the impl trait (it is early bound). If it becomes late bound, that is no longer true, so we would require the f(None)
call to use an explicit type annotation (e.g., f(None::<i32>)
). This form of breakage is permitted by our semver rules. We judge the likelihood of this impacting many crates to be small, but we will have to test it.
Implication: Backwards incompatibility around inference
Drawbacks
Giving values for uncaptured parameters
Impl trait in return position do not capture all
Rationale and alternatives
Why not introduce the typeof
keyword?
It has been proposed to use the typeof
keyword to permit users to take the resulting type from arbitrary expressions. This would mean that one could
#![allow(unused)] fn main() { fn foo<T> { } }
Why not introduce a named type
Why not introduce a named type into the environment?
It is difficult to decide
Prior art
Discuss prior art, both the good and the bad, in relation to this proposal. A few examples of what this can include are:
- For language, library, cargo, tools, and compiler proposals: Does this feature exist in other programming languages and what experience have their community had?
- For community proposals: Is this done by some other community and what were their experiences with it?
- For other teams: What lessons can we learn from what other communities have done here?
- Papers: Are there any published papers or great posts that discuss this? If you have some relevant papers to refer to, this can serve as a more detailed theoretical background.
This section is intended to encourage you as an author to think about the lessons from other languages, provide readers of your RFC with a fuller picture. If there is no prior art, that is fine - your ideas are interesting to us whether they are brand new or if it is an adaptation from other languages.
Note that while precedent set by other languages is some motivation, it does not on its own motivate an RFC. Please also take into consideration that rust sometimes intentionally diverges from common language features.
Unresolved questions
- What parts of the design do you expect to resolve through the RFC process before this gets merged?
- What parts of the design do you expect to resolve through the implementation of this feature before stabilization?
- What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC?
Future possibilities
Think about what the natural extension and evolution of your proposal would be and how it would affect the language and project as a whole in a holistic way. Try to use this section as a tool to more fully consider all possible interactions with the project and language in your proposal. Also consider how this all fits into the roadmap for the project and of the relevant sub-team.
This is also a good place to "dump ideas", if they are out of scope for the RFC you are writing but otherwise related.
If you have tried and cannot think of any future possibilities, you may simply state that you cannot think of anything.
Note that having something written down in the future-possibilities section is not a reason to accept the current or a future RFC; such notes should be in the section on motivation or rationale in this or subsequent RFCs. The section merely provides additional information.
😕 Frequently asked questions
This page lists frequently asked questions about the design. It often redirects to the other pages on the site.
What is the goal of this initiative?
See the Charter.
Who is working on it!
See the Charter.