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

use std::sync::Arc;

struct Blob<T: ?Sized>(Arc<T>);
struct Foo;
trait Bar {}
impl Bar for Foo {}

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

I suspected this had to do with covaraince, so I had a look at the section of the Nomicon, but that seems to indicate that Blob should just inherit variance from the field, which it seemingly doesn't.

I've looked into this previously and thought there was an issue for it being a feature in the future, but can't find anything relevant on GH. Is Arc somehow magical here?

Variance is only relevant for lifetimes, not type generics. This doesn't work because Blob doesn't implement CoerceUnsized, but Arc does. Arc is special since it's one of the only types that implements CoerceUnsized on stable rust. If you want to do this, you need to deconstruct and reconstruct Blob.

let bar: Blob<dyn Bar> = Blob(foo.0);
1 Like

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

In case you just want the TL;DR solution, the last code block in @steffahn's reply is an alternative to @drewtato's solution (and what I tend to use).

N.b. this changes the type of foo to be Blob<dyn Bar>,[1] in contrast with foo being Blob<Foo>.


  1. as pointed out above the quoted code ↩︎

Thank you, CoerceUnsized was the magic word I was looking for and couldn't find. Unfortunately this particular solution doesn't work because I'm writing a library where I want to pass out a Blob to the caller without them peeking inside at its contents.

That's unfortuntate, because that was almost exactly the goal. (I wanted a library function that returned Blob<Foo> and let the user turn it into a Blob<dyn Bar> if they wanted to, but I assume that falls into the same issue) Thank you for that very thorough explanation of the inference rules though, I didn't realize it would work so long as the value doesn't "land" in the Arc as a Foo

There’s a crate that might be useful to expose CoerceUnsized-like abilities to users: crates.io: Rust Package Registry

If you have a specific trait in mind, you could also supply a helper method.

impl<'t, T: 't + Bar> Blob<T> {
    fn as_dyn(self) -> Blob<dyn Bar + 't> {
        Blob(self.0)
    }
}

(Or if the Arc<_> isn't an implementation detail, just have some way to extract it.)

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.