Opaque types (impl Trait)
This chapter describes how "opaque types" are modeled in chalk. Opaque types are
the underlying concept used to implement "existential impl Trait
" in Rust.
They don't have a direct surface syntax, but uses of impl Trait
in particular
source locations create a hidden opaque type:
fn as_u32s<'a, T: Copy + Into<u32>>(
elements: &'a [T],
) -> impl Iterator<Item = u32> + 'a {
elements.iter().cloned().map(|e| -> u32 { e.into() })
}
fn main() {
let x: &[u16] = &[1, 2, 3];
let y = as_u32s(&x);
for e in y {
println!("e = {}", e);
}
}
Conceptually, the function as_u32s
is desugared to return a reference to an
opaque type, let's call it AsU32sReturn
(note that this is not valid
Rust syntax):
opaque type AsU32sReturn<'a, T>: IntoIterator<Item = u32> + 'a
where
T: Copy + Into<u32>;
fn as_u32s<'a, T: Copy + Into<u32>>(
elements: &'a [T],
) -> AsU32sReturn<'a, T> {
...
}
Opaque types are a kind of type alias. They are called opaque because, unlike
an ordinary type alias, most Rust code (e.g., the callers of as_u32s
) doesn't
know what type AsU32sReturn
represents. It only knows what traits that type
implements (e.g., IntoIterator<Item = u32>
). The actual type that is inferred
for AsU32sReturn
is called the "hidden type".
Chalk syntax for an opaque type declaration
Although the above is not valid Rust syntax, it is quite close to the format that chalk unit tests use, which looks something like this:
opaque type OpaqueTypeName<P0..Pn>: /* bounds */
where
/* where clauses */
= /* hidden type */;
A chalk opaque type declaration has several parts:
- The name
OpaqueTypeName
, which is the name we use to refer to the opaque type within the chalk file. In real Rust opaque types are not explicitly declared and hence they are identified just by internal ids (i.e., they are anonymous in the same way that a closure type is anonymous), so this is just for unit testing. - The generic parameters
P0..Pn
. In real Rust, these parameters are inherited from the context in which theimpl Trait
appeared. In our example, these parameters come from the surrounding function. Note that in real Rust the set of generic parameters is a subset of those that appear on the surrounding function: in particular, lifetime parameters may not appear unless they explicitly appear in the opaque type's bounds. - The bounds, which would be
IntoIterator<Item = u32> + 'a
in our example. These are traits that the hidden type (see below) is supposed to implement. They come from theimpl IntoIterator<Item = u32> + 'a
type. Even when the hidden type is, well, hidden, we can assume that the bounds hold. - The where clauses, which would be
T: Copy
andT: Into<u32>
in our example. These are conditions that must hold onV0..Vn
forOpaqueTypeName<V0..Vn>
to be a valid type.- Note that this contrasts with bounds: bounds are things that the hidden type must meet
but which the rest of the code can assume to be true. Where clauses are things
that the rest of the code must prove to be true in order to use the opaque type.
In our example, then, a type like
AsU32sReturn<'a, String>
would be invalid becauseString: Copy
does not hold.
- Note that this contrasts with bounds: bounds are things that the hidden type must meet
but which the rest of the code can assume to be true. Where clauses are things
that the rest of the code must prove to be true in order to use the opaque type.
In our example, then, a type like
Representing opaque types in chalk types
We represent opaque types as a kind of type alias. Like any type alias, we have to define the conditions in which they can be normalized: