impl Trait
Existential types in Rust
I’m a little embarrassed that I’ve been writing Rust for the last four years and
just now realized that fn foo(arg: impl Trait) is not a function accepting a
trait object but is in fact exactly the same as fn foo<T: Trait>(arg: T).
1 It must have been a recent change, right? Oh,
2018. Anyways,
why was this syntax added? I don’t think that this syntax is enough of an
improvement 2 to add the complexity of a second way of doing things.
It turns out that there were a lot of reasons, 3 but one of the main
reasons is symmetry with impl Trait in the return position, i.e. fn foo() -> impl Trait. This is more interesting because it is not equivalent to fn foo<T: Trait>() -> T. In fact, it is the complete opposite: a function returning an
impl Trait can only return a single concrete type.
In type theory this is an existential type, where the caller only knows that
a type exists that satisfies the given abstract type. 4 In Rust,
this allows the compiler to use the returned type as if it was a concrete type
(e.g. static dispatch, no boxing) while hiding that from the caller. The impl Trait in the argument position is a universal type, where the callee must
accept all types that satisfy the abstract type.
Are there any real-world uses for impl Trait as a return type? There is one
very prominent (but not obvious) example—async functions actually return an
impl Future<Output=T>. Because futures have unique, un-writable types, they
can only be returned either as impl Future<Output=T> or as Box<dyn Future<Output=T>>, and so the impl Trait feature allows Rust to have
stack-allocated futures.
Anyways, I doubt I will be writing functions that return an impl Trait very
often, but it’s a really neat example of how Rust enables zero-cost
abstractions.
-
I blame the similarity to
dyn Trait.fn foo<T: Trait>(arg: T)is so obviously generic whilefn foo(arg: impl Trait)(generic) looks very similar tofn foo(arg: Box<dyn Trait>)(not generic). I think I just sawTraitin the argument position and assumed it was the latter. ↩ -
Actually, I much prefer the more verbose syntax. It feels much more intentional. ↩
-
In Rust, a function that returns an existential type can only return a single concrete type, but that is not a general fact about existential types. ↩