In Rust, "traits" and "types" are entirely separate. You cannot write struct Foo(SomeTrait) or fn bar(arg: SomeTrait)[1]. A trait can be converted into a type in three ways:
- Bounded type parameter:
<E: Trait>, then E is a type
- Trait object:
dyn Trait
- Existential type:
impl Trait
A trait consists of types, constants, and functions, which are called associated items. Every type that implements the trait does so by providing a definition for each associated item.[2] For example, a trait contains a function signature, and every type that implements the trait defines a function with the same signature and a different body.
An existential type is a type that implements a particular trait and is opaque. What this means is roughly, you can call the trait's methods and use its other associated items on the instance, and pass the instance to functions/constructors that take a generic or existential type that implements said trait or a supertrait (or functions/constructors that take the exact same existential), but nothing else.
// A trait
trait SomeTrait {
// An associated function
fn value(&self) -> u32;
}
// A type
struct Foo(&'static str);
// Another type
struct Bar(u32);
// An implementation. This one makes it so `Foo` implements `SomeTrait`
impl SomeTrait for Foo {
// Same signature as in `SomeTrait`: `fn value(&self) -> u32`
fn value(&self) -> u32 {
self.0.len() as u32
}
}
// This implementation makes it so `Bar` implements `SomeTrait`
impl SomeTrait for Bar {
// Same signature: `fn value(&self) -> u32`
fn value(&self) -> u32 {
// Different body
self.0
}
}
// This function returns an existential type.
fn helper() -> impl SomeTrait {
Foo("Hello")
}
fn main() {
let foo = helper();
// Even though at runtime, `foo` is guaranteed to be type `Foo`,
// the type returned by `helper` is "opaque" in the sense that we
// cannot use it like so. The following would be a compiler error:
// > println!("{}", foo.0);
// We can only use it like any other type that implements `SomeTrait`;
// this means we can only call `foo.value()`
println!("{}", foo.value()); // Prints "5"
}
More formally, every existential type O that implements trait T has a corresponding precise type P (which may itself be existential, or is an implicit type parameter if the existantial type is in a parameter). There's a limited scope[3] in which you P and O can be used interchangeably, and instances of one can be assigned to places that expect the other. Outside of that scope, instances of P and O have no relation, and O can only be used as a type with a hidden structure that implements trait T
[1] You could write these prior to Rust 2021, but traits and types were still separate. Trait in type position was just syntax for a trait object (equivalent to today's dyn Trait), and this confusion is partly why it was deprecated and replaced by dyn Trait.
[2] Except associated items with default definitions. If a type doesn't provide an definition for one of those, it uses its default definition.
[3] The "limited scope" depends on the existential type's occurrence's location: whether the existential type is a parameter type, return type, let-binding type annotation, associated type definition, or type alias.
// Existential parameter: you can pass any type implementing `SomeTrait` to it,
// but inside the function body it's opaque.
fn foo(a: impl SomeTrait, i: usize) -> u32 {
if i == 0 {
a.value()
} else {
3 + foo(a, i - 1)
}
}
fn use_foo() {
println!("{}", foo(Foo("Hello"), 4)); // Prints "17"
println!("{}", foo(Bar(5), 10)); // Prints "35"
}
// Existential return type: you can return any type implementing `SomeTrait`,
// but outside the function body it's opaque.
// Also, you cannot return different types in the same function
// (e.g. `if (i == 0) { Foo("Hello") } else { Bar(i) }` is illegal).
fn bar(i: u32) -> impl SomeTrait {
Bar(i * 10)
}
fn use_bar() {
println!("{}", bar(10).value()); // Prints "100"
println!("{}", foo(bar(20), 4)); // Prints "212"
}
fn baz() {
// Existential `let`-binding type annotation: you can assign any type,
// but the bound identifier is opaque.
// Also, you cannot assign different types in the same `let` binding.
// Requires `#![feature(impl_trait_in_bindings)]`, which as of Rust 1.82 is not implemented,
// so the following actually won't compile:
// > let x: impl SomeTrait = Foo("Hello");
// But we can get almost the same behavior with the following code,
// which itself requires `#![feature(type_alias_impl_trait)]`
type X = impl SomeTrait;
let x: X = Foo("Hello");
// The following is invalid:
// > println!("{}", x.0);
// This is OK:
println!("{}", x.value()); // Prints "5"
// As is this:
println!("{}", foo(x, 10)); // Prints "35"
}
struct Baz(u32);
impl Iterator for Baz {
// Existential associated type: inside the `impl` block it's a precise type,
// outside the `impl` block it's opaque.
// It's also required in order to define an associated type to be an unnamed existential;
// I cannot define `Item` to be anything else or `next` won't compile.
// Requires `#![feature(impl_trait_in_assoc_type)]`
type Item = impl SomeTrait;
fn next(&mut self) -> Option<Self::Item> {
if self.0 == 0 {
return None;
}
let i = self.0;
self.0 -= 1;
// Here's an example where existential types are required:
// the type we return is an existential type.
Some(bar(i))
}
// (Again, if there were other `Self::Item`s within the `impl` block,
// they would all have to be the same type,
// which in this case is a return value of a call to `bar`.)
}
mod qux {
use super::*;
// Existential type alias: inside the module it's a precise type,
// outside the module it's opaque.
// It's also required in order to use an unnamed existential in a named position without making it a generic;
// I cannot define `T` to be anything else or `Ts` won't compile.
// Requires `#![feature(type_alias_impl_trait)]`
pub type T = impl SomeTrait;
pub struct Ts {
pub x: T,
pub y: T
}
pub fn ts() -> Ts {
Ts {
x: Baz(100).next().unwrap(),
y: Baz(200).next().unwrap()
}
}
}
fn use_qux() {
let ts = qux::ts();
println!("{}", ts.x.value()); // "1000"
println!("{}", ts.y.value()); // "2000"
}
```