For ordinary functions something like foo<T>() -> impl SomeTrait { … }
means
-
foo
has a fixed known-at-compile-time return type (so that in particular there is no overhead at run-time compared to e.g. using dyn SomeTrait
– well regarding “fixed”: it can depend on generic type arguments like the argument T
here (also it can only depend on the type arguments, nothing else well, besides lifetime arguments, but the rules are slightly complicated)
- the return type of
foo
is however left unspecified to the API user, except for the information that it implements the trait SomeTrait
The benefit of this future over the approach to “just write the return type explicitly” can be
- the return type can be changed in the future without breaking API changes
- this can otherwise only be achieved by creating a wrapper struct that abstracts away the concrete type
- the return type can be an unnameable type like e.g. the type of a closure or the type of an
async { … }
block
- as such,
impl Trait
-return types are commonly used for -> impl Fn(Foo) -> Bar
or -> impl Future<Output = Foo>
; but also for impl Iterator<Item = Baz>
, because iterators created by methods like .map()
also contain closure types. For the case of fn foo(arguments...) -> impl Future<Output = T>
, there's also the more convenient, but mostly equivalent alternative of writing async fn foo(arguments...) -> T
- the return type might be a very complex type, and writing
impl SomeTrait
makes the API much cleaner / easier to read
- of course, in this case just writing the lengthy type is always an alternative
One downside of -> impl Trait
is that this return type itself becomes unnamable, too; this is why one sometimes wants to avoid impl Trait
return types in cases where it's possible, like for 1. or 3. above. This shortcoming is going to be addressed by an unstable language feature called type_alias_impl_trait
.
Back to traits: The natural extension of the rule above, where the return type was compile-time-known, depending on the generic arguments, for methods in a trait would be:
In trait Foo<S> { fn foo<T>() -> impl SomeTrait; }
, the return type can depend on Self
, S
, and T
, i.e. on Self
, as well as the generic arguments of the trait and the method.
And that describes the relation to associated types: The way to define a type in a trait that depends on Self
and the generic arguments of the trait is precisely done by using an associated type. If you also need dependence on additional generic arguments, like the generic arguments to the method, then you need generic associated types. The additional details of the impl Trait
-feature regarding unnameable types, and the fact that the type is left unspecified to the API user, are mere technicalities that could also, alternatively, be achieved by using type_alias_impl_trait
in an associated type.
For more notes on current developments / ideas / designwork on support for -> impl Trait
return types in traits, see e.g. this page
impl Trait in traits - wg-async-foundations
and the stuff it links to, particularly the "exploration doc" at the bottom. The connection to async
is, as mentioned above, that async fn
(pretty much) desugars to -> impl Future<Output = ...>
, and there's a high demand for supporting async fn
in trait methods. In case you're interested in async fn
in trait, also take a look at
why async fn in traits are hard - Baby Steps
most of the stuff listed there also relates to the more general case beyond just async fn
.