319 lines
12 KiB
Markdown
319 lines
12 KiB
Markdown
# Variance of type and lifetime parameters
|
||
|
||
<!-- toc -->
|
||
|
||
For a more general background on variance, see the [background] appendix.
|
||
|
||
[background]: ./appendix/background.html
|
||
|
||
During type checking we must infer the variance of type and lifetime
|
||
parameters. The algorithm is taken from Section 4 of the paper ["Taming the
|
||
Wildcards: Combining Definition- and Use-Site Variance"][pldi11] published in
|
||
PLDI'11 and written by Altidor et al., and hereafter referred to as The Paper.
|
||
|
||
[pldi11]: https://people.cs.umass.edu/~yannis/variance-extended2011.pdf
|
||
|
||
This inference is explicitly designed *not* to consider the uses of
|
||
types within code. To determine the variance of type parameters
|
||
defined on type `X`, we only consider the definition of the type `X`
|
||
and the definitions of any types it references.
|
||
|
||
We only infer variance for type parameters found on *data types*
|
||
like structs and enums. In these cases, there is a fairly straightforward
|
||
explanation for what variance means. The variance of the type
|
||
or lifetime parameters defines whether `T<A>` is a subtype of `T<B>`
|
||
(resp. `T<'a>` and `T<'b>`) based on the relationship of `A` and `B`
|
||
(resp. `'a` and `'b`).
|
||
|
||
We do not infer variance for type parameters found on traits, functions,
|
||
or impls. Variance on trait parameters can indeed make sense
|
||
(and we used to compute it) but it is actually rather subtle in
|
||
meaning and not that useful in practice, so we removed it. See the
|
||
[addendum] for some details. Variances on function/impl parameters, on the
|
||
other hand, doesn't make sense because these parameters are instantiated and
|
||
then forgotten, they don't persist in types or compiled byproducts.
|
||
|
||
[addendum]: #addendum
|
||
|
||
> **Notation**
|
||
>
|
||
> We use the notation of The Paper throughout this chapter:
|
||
>
|
||
> - `+` is _covariance_.
|
||
> - `-` is _contravariance_.
|
||
> - `*` is _bivariance_.
|
||
> - `o` is _invariance_.
|
||
|
||
## The algorithm
|
||
|
||
The basic idea is quite straightforward. We iterate over the types
|
||
defined and, for each use of a type parameter `X`, accumulate a
|
||
constraint indicating that the variance of `X` must be valid for the
|
||
variance of that use site. We then iteratively refine the variance of
|
||
`X` until all constraints are met. There is *always* a solution, because at
|
||
the limit we can declare all type parameters to be invariant and all
|
||
constraints will be satisfied.
|
||
|
||
As a simple example, consider:
|
||
|
||
```rust,ignore
|
||
enum Option<A> { Some(A), None }
|
||
enum OptionalFn<B> { Some(|B|), None }
|
||
enum OptionalMap<C> { Some(|C| -> C), None }
|
||
```
|
||
|
||
Here, we will generate the constraints:
|
||
|
||
```text
|
||
1. V(A) <= +
|
||
2. V(B) <= -
|
||
3. V(C) <= +
|
||
4. V(C) <= -
|
||
```
|
||
|
||
These indicate that (1) the variance of A must be at most covariant;
|
||
(2) the variance of B must be at most contravariant; and (3, 4) the
|
||
variance of C must be at most covariant *and* contravariant. All of these
|
||
results are based on a variance lattice defined as follows:
|
||
|
||
```text
|
||
* Top (bivariant)
|
||
- +
|
||
o Bottom (invariant)
|
||
```
|
||
|
||
Based on this lattice, the solution `V(A)=+`, `V(B)=-`, `V(C)=o` is the
|
||
optimal solution. Note that there is always a naive solution which
|
||
just declares all variables to be invariant.
|
||
|
||
You may be wondering why fixed-point iteration is required. The reason
|
||
is that the variance of a use site may itself be a function of the
|
||
variance of other type parameters. In full generality, our constraints
|
||
take the form:
|
||
|
||
```text
|
||
V(X) <= Term
|
||
Term := + | - | * | o | V(X) | Term x Term
|
||
```
|
||
|
||
Here the notation `V(X)` indicates the variance of a type/region
|
||
parameter `X` with respect to its defining class. `Term x Term`
|
||
represents the "variance transform" as defined in the paper:
|
||
|
||
> If the variance of a type variable `X` in type expression `E` is `V2`
|
||
and the definition-site variance of the corresponding type parameter
|
||
of a class `C` is `V1`, then the variance of `X` in the type expression
|
||
`C<E>` is `V3 = V1.xform(V2)`.
|
||
|
||
## Constraints
|
||
|
||
If I have a struct or enum with where clauses:
|
||
|
||
```rust,ignore
|
||
struct Foo<T: Bar> { ... }
|
||
```
|
||
|
||
you might wonder whether the variance of `T` with respect to `Bar` affects the
|
||
variance `T` with respect to `Foo`. I claim no. The reason: assume that `T` is
|
||
invariant with respect to `Bar` but covariant with respect to `Foo`. And then
|
||
we have a `Foo<X>` that is upcast to `Foo<Y>`, where `X <: Y`. However, while
|
||
`X : Bar`, `Y : Bar` does not hold. In that case, the upcast will be illegal,
|
||
but not because of a variance failure, but rather because the target type
|
||
`Foo<Y>` is itself just not well-formed. Basically we get to assume
|
||
well-formedness of all types involved before considering variance.
|
||
|
||
### Dependency graph management
|
||
|
||
Because variance is a whole-crate inference, its dependency graph
|
||
can become quite muddled if we are not careful. To resolve this, we refactor
|
||
into two queries:
|
||
|
||
- `crate_variances` computes the variance for all items in the current crate.
|
||
- `variances_of` accesses the variance for an individual reading; it
|
||
works by requesting `crate_variances` and extracting the relevant data.
|
||
|
||
If you limit yourself to reading `variances_of`, your code will only
|
||
depend then on the inference of that particular item.
|
||
|
||
Ultimately, this setup relies on the [red-green algorithm][rga]. In particular,
|
||
every variance query effectively depends on all type definitions in the entire
|
||
crate (through `crate_variances`), but since most changes will not result in a
|
||
change to the actual results from variance inference, the `variances_of` query
|
||
will wind up being considered green after it is re-evaluated.
|
||
|
||
[rga]: ./queries/incremental-compilation.html
|
||
|
||
<a id="addendum"></a>
|
||
|
||
## Addendum: Variance on traits
|
||
|
||
As mentioned above, we used to permit variance on traits. This was
|
||
computed based on the appearance of trait type parameters in
|
||
method signatures and was used to represent the compatibility of
|
||
vtables in trait objects (and also "virtual" vtables or dictionary
|
||
in trait bounds). One complication was that variance for
|
||
associated types is less obvious, since they can be projected out
|
||
and put to myriad uses, so it's not clear when it is safe to allow
|
||
`X<A>::Bar` to vary (or indeed just what that means). Moreover (as
|
||
covered below) all inputs on any trait with an associated type had
|
||
to be invariant, limiting the applicability. Finally, the
|
||
annotations (`MarkerTrait`, `PhantomFn`) needed to ensure that all
|
||
trait type parameters had a variance were confusing and annoying
|
||
for little benefit.
|
||
|
||
Just for historical reference, I am going to preserve some text indicating how
|
||
one could interpret variance and trait matching.
|
||
|
||
### Variance and object types
|
||
|
||
Just as with structs and enums, we can decide the subtyping
|
||
relationship between two object types `&Trait<A>` and `&Trait<B>`
|
||
based on the relationship of `A` and `B`. Note that for object
|
||
types we ignore the `Self` type parameter – it is unknown, and
|
||
the nature of dynamic dispatch ensures that we will always call a
|
||
function that is expected the appropriate `Self` type. However, we
|
||
must be careful with the other type parameters, or else we could
|
||
end up calling a function that is expecting one type but provided
|
||
another.
|
||
|
||
To see what I mean, consider a trait like so:
|
||
|
||
```rust
|
||
trait ConvertTo<A> {
|
||
fn convertTo(&self) -> A;
|
||
}
|
||
```
|
||
|
||
Intuitively, If we had one object `O=&ConvertTo<Object>` and another
|
||
`S=&ConvertTo<String>`, then `S <: O` because `String <: Object`
|
||
(presuming Java-like "string" and "object" types, my go to examples
|
||
for subtyping). The actual algorithm would be to compare the
|
||
(explicit) type parameters pairwise respecting their variance: here,
|
||
the type parameter A is covariant (it appears only in a return
|
||
position), and hence we require that `String <: Object`.
|
||
|
||
You'll note though that we did not consider the binding for the
|
||
(implicit) `Self` type parameter: in fact, it is unknown, so that's
|
||
good. The reason we can ignore that parameter is precisely because we
|
||
don't need to know its value until a call occurs, and at that time (as
|
||
you said) the dynamic nature of virtual dispatch means the code we run
|
||
will be correct for whatever value `Self` happens to be bound to for
|
||
the particular object whose method we called. `Self` is thus different
|
||
from `A`, because the caller requires that `A` be known in order to
|
||
know the return type of the method `convertTo()`. (As an aside, we
|
||
have rules preventing methods where `Self` appears outside of the
|
||
receiver position from being called via an object.)
|
||
|
||
### Trait variance and vtable resolution
|
||
|
||
But traits aren't only used with objects. They're also used when
|
||
deciding whether a given impl satisfies a given trait bound. To set the
|
||
scene here, imagine I had a function:
|
||
|
||
```rust,ignore
|
||
fn convertAll<A,T:ConvertTo<A>>(v: &[T]) { ... }
|
||
```
|
||
|
||
Now imagine that I have an implementation of `ConvertTo` for `Object`:
|
||
|
||
```rust,ignore
|
||
impl ConvertTo<i32> for Object { ... }
|
||
```
|
||
|
||
And I want to call `convertAll` on an array of strings. Suppose
|
||
further that for whatever reason I specifically supply the value of
|
||
`String` for the type parameter `T`:
|
||
|
||
```rust,ignore
|
||
let mut vector = vec!["string", ...];
|
||
convertAll::<i32, String>(vector);
|
||
```
|
||
|
||
Is this legal? To put another way, can we apply the `impl` for
|
||
`Object` to the type `String`? The answer is yes, but to see why
|
||
we have to expand out what will happen:
|
||
|
||
- `convertAll` will create a pointer to one of the entries in the
|
||
vector, which will have type `&String`
|
||
- It will then call the impl of `convertTo()` that is intended
|
||
for use with objects. This has the type `fn(self: &Object) -> i32`.
|
||
|
||
It is OK to provide a value for `self` of type `&String` because
|
||
`&String <: &Object`.
|
||
|
||
OK, so intuitively we want this to be legal, so let's bring this back
|
||
to variance and see whether we are computing the correct result. We
|
||
must first figure out how to phrase the question "is an impl for
|
||
`Object,i32` usable where an impl for `String,i32` is expected?"
|
||
|
||
Maybe it's helpful to think of a dictionary-passing implementation of
|
||
type classes. In that case, `convertAll()` takes an implicit parameter
|
||
representing the impl. In short, we *have* an impl of type:
|
||
|
||
```text
|
||
V_O = ConvertTo<i32> for Object
|
||
```
|
||
|
||
and the function prototype expects an impl of type:
|
||
|
||
```text
|
||
V_S = ConvertTo<i32> for String
|
||
```
|
||
|
||
As with any argument, this is legal if the type of the value given
|
||
(`V_O`) is a subtype of the type expected (`V_S`). So is `V_O <: V_S`?
|
||
The answer will depend on the variance of the various parameters. In
|
||
this case, because the `Self` parameter is contravariant and `A` is
|
||
covariant, it means that:
|
||
|
||
```text
|
||
V_O <: V_S iff
|
||
i32 <: i32
|
||
String <: Object
|
||
```
|
||
|
||
These conditions are satisfied and so we are happy.
|
||
|
||
### Variance and associated types
|
||
|
||
Traits with associated types – or at minimum projection
|
||
expressions – must be invariant with respect to all of their
|
||
inputs. To see why this makes sense, consider what subtyping for a
|
||
trait reference means:
|
||
|
||
```text
|
||
<T as Trait> <: <U as Trait>
|
||
```
|
||
|
||
means that if I know that `T as Trait`, I also know that `U as
|
||
Trait`. Moreover, if you think of it as dictionary passing style,
|
||
it means that a dictionary for `<T as Trait>` is safe to use where
|
||
a dictionary for `<U as Trait>` is expected.
|
||
|
||
The problem is that when you can project types out from `<T as
|
||
Trait>`, the relationship to types projected out of `<U as Trait>`
|
||
is completely unknown unless `T==U` (see #21726 for more
|
||
details). Making `Trait` invariant ensures that this is true.
|
||
|
||
Another related reason is that if we didn't make traits with
|
||
associated types invariant, then projection is no longer a
|
||
function with a single result. Consider:
|
||
|
||
```rust,ignore
|
||
trait Identity { type Out; fn foo(&self); }
|
||
impl<T> Identity for T { type Out = T; ... }
|
||
```
|
||
|
||
Now if I have `<&'static () as Identity>::Out`, this can be
|
||
validly derived as `&'a ()` for any `'a`:
|
||
|
||
```text
|
||
<&'a () as Identity> <: <&'static () as Identity>
|
||
if &'static () < : &'a () -- Identity is contravariant in Self
|
||
if 'static : 'a -- Subtyping rules for relations
|
||
```
|
||
|
||
This change otoh means that `<'static () as Identity>::Out` is
|
||
always `&'static ()` (which might then be upcast to `'a ()`,
|
||
separately). This was helpful in solving #21750.
|