What's the (variance?) difference between these snippets?

This is actually about unsizing coercions. And also about type inference and implicit coercions.

Type inference in the presence of implicit coercions is a bit hard. Looking at something like

    let foo = Arc::new(Foo);
    let bar: Arc<dyn Bar> = foo;

it seems feasable to coerce in many. Think of it as

fn works() {
    let foo = coerce(Arc::new(coerce(Foo)));
    let bar: Arc<dyn Bar> = coerce(foo);
}

where coerce is supposed to indicate a possible implicit coercion site. The issue now is, the compiler knows the type of Foo: Foo the signature of Arc::new: fn<T>(T) -> Arc<T> and the final type Arc<dyn Bar>. This leaves much information to be desired.

Foo can be coerced from type Foo to any compatible type T1. Then Arc::new creates Arc<T1>, which could be coerced to any type T2 compatible with coercion from Arc<T1>. foo: T2 would then be finally coerced into Arc<dyn Bar>.

The compiler solves this predicament, as far as I’m aware (without ever having read the relevant actual implementation by eliminating possible coercions, essentially, if the source and target types are not yet known. (Or rather, not yet known to be different.) This happens in order of appearance in code, as far as I’m aware.

So the first coerce from Arc<T1> to T2 gets eliminated because those could still be the same type

fn works() {
    let foo = Arc::new(coerce(Foo));
    let bar: Arc<dyn Bar> = coerce(foo);
}

then we know foo has type Arc<T1>. The second coerce is still from Foo to unknown T1, which can still be the same, so it’s eliminated, too

fn works() {
    let foo = Arc::new(Foo);
    let bar: Arc<dyn Bar> = coerce(foo);
}

and foo is known to be Arc<Foo>. Finally the remaining coercion is known that it needs to coerce Arc<Foo> into Arc<dyn Bar>, two different types. This coercion stays. And also, it works.


Similarly for

fn fails() {
    let foo = coerce(Blob(coerce(Arc::new(coerce(Foo))));
    let bar: Blob<dyn Bar> = coerce(foo);
}

all coercions in the first line are gone eventually,

fn fails() {
    let foo = Blob(Arc::new(Foo);
    let bar: Blob<dyn Bar> = coerce(foo);
}

leaving the second line with the task to coerce Blob<Foo> into Blob<dyn Bar>. This is what fails.

This is unrelated to variance, as unsizing coercions are not working with variance. The way this coercion could be supported is if Blob had an implementation of CoerceUnsized, on nightly Rust, using unstable features; otherwise, unsizing coercions can only handle a handful of pointer types (Box, Arc, Rc, references), potentially surrounded by Pin and/or a few other types like Cell.


Ways to fix the second code example exist. All we need is to make the compiler decide to actually do the coercion before the value is wrapped in the Blob constructor. For example

fn doesnt_fail() {
    let foo: Blob<dyn Bar> = Blob(Arc::new(Foo));
    let bar: Blob<dyn Bar> = foo;
}

works by virtue of how this whole implicit coercion business works:

fn doesnt_fail() {
    let foo: Blob<dyn Bar> = coerce(Blob(coerce(Arc::new(coerce(Foo)))));
    let bar: Blob<dyn Bar> = coerce(foo);
}

In the first line the types are: Foo coerces to T1, gets wrapped with Arc::new to give Arc<T1>, coerced to T2, this gets wrapped by Blob… well Blob expects something of the form Arc<T3> so actually T2 == Arc<T3>, and returns Blob<T3>, which coerces to Blob<dyn Bar>.

First coerce is eliminated (the last in that chain) because Blob<T3> and Blob<dyn Bar> can be the same type.

fn doesnt_fail() {
    let foo: Blob<dyn Bar> = Blob(coerce(Arc::new(coerce(Foo))));
    let bar: Blob<dyn Bar> = coerce(foo);
}

So T3 == dyn Bar. Next coerce is now from Arc<T1> to Arc<dyn Bar>. That one doesn’t get eliminated. Why? Don’t ask me. My working hypothesis was that Arc::new returning a Sized type might help making the deduction that Arc<T1> and Arc<dyn Bar> aren’t the same type. (Though some experimentation I just did suggests that the way this Sized-ness is deduced is a bit … “special”…. Or I’m following a complete red herring here, and it works differently, entirely.)


Anyways… of course, this whole workaround cannot work if your goal was that foo has type Blob<Foo> not Blob<dyn Bar>. Maybe I should’ve mentioned that earlier. If you want foo to still be Blob<Foo>, then @drewtato described the necessary solution above.

If you do permit foo: Blob<dyn Bar>, a reliable way to help out the compiler with putting the coercion into the right place is with explicit as casts. as casts support all implicit coercions (like unsizing), too, but they can never be eliminated to help type inference out. Instead, if an as cast stays unknown, it gives an error.

fn demo() {
    let x: i32 = 0;
    let y = x as _;
}
error[E0282]: type annotations needed
  --> src/lib.rs:26:9
   |
26 |     let y = x as _;
   |         ^        - type must be known at this point

But this helps us… we can re-write fails() as follows

fn doesnt_fail() {
    let foo = Blob(Arc::new(Foo) as _);
    let bar: Blob<dyn Bar> = foo;
}

and the code works, because the place where Arc<Foo> to Arc<dyn Bar> coercion happens no longer needs an implicit coercion.

3 Likes