Assign concrete data to `Arc<Mutex<Box<dyn T>>>` field

Straight to the point: Why example A is okay while example B is not?

Could anyone explain the error behind it and how to make it work?

Thank you in advance.

I think it's just something weird with type inference/coercions - if you add an explicit cast:

let user = Arc::new(Mutex::new(Box::new(GuestUser {}) as Box<dyn User>));

Then it works.

2 Likes

In cases like this where coercion didn't happen because of how type inference works, it's typically sufficient to insert just a single as _ at the place you want the coercion to happen. Rust Playground

2 Likes

Ah, I didn't know that we could do that. Thanks!

But why example A, coercion is not required? Is that any reason behind it or just a quirk?

In a chain of function calls like

        let user = Arc::new(Mutex::new(Box::new(GuestUser {})));
        Client { user }

if user passed into the Client { user } constructor should be an Arc<Mutex<Box<dyn User>>> how does this come to be?

GuestUser {} is a GuestUser,
then … maybe … that coerces to some other type T or it stays GuestUser,
the result is passed to Box::new which creates a Box<T>… or Box<GuestUser> if no coercion happened,
then … maybe … that result that’s either Box<T> or Box<GuestUser> coerces to some other type or stay the same,
the result is passed to Mutex::new

… and so on.

TL;DR, there’s a lot of possible places where coercions could theoretically happen, and if you think about it, this many possibilities completely breaks type inference!

The way that rust makes sure that type inference doesn’t break is by simply throwing out the possibility of coercions fairly quickly. I don’t really know how the “actual” rules for this are, but from my experience, when you call a generic function foo(BAR) in a way where the argument type isn’t completely clear (yet), then the compiler just starts to rule out the possibility for the argument BAR to be coerced. With that said, on second thought, I am actually somewhat surprised that this manages to compile if you write Client { user: Arc::new(Mutex::new(Box::new(GuestUser {}))) }, so there seems to be some existing logic in the compiler that figures out that the Box is the only thing that can coerce here in a sensible manner; with this in mind, I wouldn’t be surprised if one day rustc starts accepting your original code as well…

…hmm having thought about this for another minute – and this is just speculation – perhaps the reasoning in the compiler is that there’s some specific order in which it gives up on coercion sites that aren’t known-yet to be actual coercions. I could imagine, essentially in order of writing the code:

The following is how I imagine type inference could work; I’m not actually familiar with any of the real implementation. But even if the real implementation is different, this illustrates how some special rules for avoiding (what would otherwise happen) that coercions make type inference fail almost all the time (with errors about ambiguous types) can, while being mostly beneficial in practice, also result in the compiler behavior reacting in subtle ways on a change such as putting user in a separate variable or not.)

I.e. for

Client { user: Arc::new(Mutex::new(Box::new(GuestUser {}))) }

which has coercion sites

Client { user: /* here */ Arc::new( /* here */ Mutex::new( /* here */ Box::new( /* here */ GuestUser {}))) }

the types are

  • user expects Arc<Mutex<Box<dyn User>>>
  • Arc::new produces some type Arc<T1> from T1
    • the resulting Arc<T1> turning into Arc<Mutex<Box<dyn User>>> may or may not be a coercion
  • Mutex::new produces some type Mutex<T2> from T2
    • the resulting Mutex<T2> turning into T1 may or may not be a coercion
  • Box::new produces some type Box<T3> from T3
    • the resulting Box<T3> turning into T2 may or may not be a coercion
  • GuestUser {} produces a GuestUser
    • GuestUser turning into T3 may or may not be a coercion

giving up on the first coercion means that

  • Arc<T1> turning into Arc<Mutex<Box<dyn User>>> is not a coercion
    • so T1 is the same type as Mutex<Box<dyn User>>
    • Mutexturning intoMutex<Box>` still may or may not be a coercion

giving up on the second coercion now, means that

  • Mutex<T2> turning into Mutex<Box<dyn User>> is not a coercion
    • so T2 is the same type as Box<dyn User>
    • Box<T3> turning into Box<dyn User> must be a coercion, because T3 is Sized! (Since Box::new can only work with Sized types)

giving up on the next coercion that is not known to be a coercion yet

  • GuestUser turning into T3 is no longer a coercion
    • so T3 and GuestUser are the same type
    • this resolves all unknown type variables T1, T2, T3, and type inference is complete
    • it remains to check that Box<GuestUser> turning into Box<dyn User> is a valid coercion, which it is

In contrast, consider

let user = Arc::new(Mutex::new(Box::new(GuestUser {})));
Client { user: user }

which has coercion sites

let user = /* here */ Arc::new( /* here */ Mutex::new( /* here */ Box::new( /* here */ GuestUser {})))
Client { user: /* here */ }

the types are

  • let user has some type T0
  • Arc::new produces some type Arc<T1> from T1
    • the resulting Arc<T1> turning into T0 may or may not be a coercion
  • Mutex::new produces some type Mutex<T2> from T2
    • the resulting Mutex<T2> turning into T1 may or may not be a coercion
  • Box::new produces some type Box<T3> from T3
    • the resulting Box<T3> turning into T2 may or may not be a coercion
  • GuestUser {} produces a GuestUser
    • GuestUser turning into T3 may or may not be a coercion
  • Client { user: … } expects Arc<Mutex<Box<dyn User>>>
    • T0 turning into Arc<Mutex<Box<dyn User>>> may or may not be a coercion

Now, we – again – start eliminating coercion sites that aren’t known to be coercion sites yet:

giving up on the first coercion means that

  • Arc<T1> turning into T0 is not a coercion
    • so T0 is the same type as Arc<T1>
    • Mutex<T2> turning into T1 still may or may not be a coercion
    • also, in the next line, Arc<T1> turning into Arc<Mutex<Box<dyn User>>> still may or may not be a coercion

giving up on the second coercion now, means that

  • Mutex<T2> turning into T1 is not a coercion
    • so T1 is the same type as Mutex<T2> (and T0 is Arc<Mutex<T2>>)
    • Box<T3> turning into T2 still may or may not be a coercion
      • this is the important difference. We don’t know yet whether or not T2 is the same type as Box<dyn User> (because we haven’t ruled out yet that “T0 turning into Arc<Mutex<Box<dyn User>>>” is a coercion) , so we cannot rule out this coercion, and it’s the next one to be elimitated…
    • also, in the next line, Arc<Mutex<T2>> turning into Arc<Mutex<Box<dyn User>>> still may or may not be a coercion

…eliminating the next coercion that is not known to be a coercion yet:

  • Box<T3> turning into T2 is not a coercion
    • so T2 is the same type as Box<T3> (in turn, T1 is Mutex<Box<T3>> and T0 is Arc<Mutex<Box<T3>>>)
    • GuestUser turning into T3 still may or may not be a coercion
    • now, in the next line, Arc<Mutex<Box<T3>>> turning into Arc<Mutex<Box<dyn User>>> has to be a coercion, since T3 is Sized. This coercion will turn out not to be valid, but we don’t check this here, and don’t use this knowledge to “retroactively” decide against eliminating the “Box<T3> turning into T2” coercion we just got rid of

eliminating the last unclear coercion

  • GuestUser turning into T3 is not a coercion
    • so T3 is the same type as GuestUser (so T0 is Arc<Mutex<Box<GuestUser>>>)
    • this resolves all unknown type variables T0, T1, T2, T3, and type inference is complete

Type-inference is complete and the single coercion we ended up with wants to convert Arc<Mutex<Box<GuestUser>>> into Arc<Mutex<Box<dyn User>>>. We check this coercion next, and it turns out, this is not a valid coercion! So we get the type mismatch error

error[E0308]: mismatched types
  --> […location…]
   |
27 |         Client { user }
   |                  ^^^^ expected trait object `dyn User`, found struct `GuestUser`
   |
   = note: expected struct `Arc<Mutex<Box<(dyn User + 'static)>>>`
              found struct `Arc<Mutex<Box<GuestUser>>>`

As a last case, without going through the whole thing: With the explicit as _, we have a cast (i.e. not just a possible implicit coercion) at the place where we want the coercion to happen, the code compiles. Casts don’t ever get eliminated the way coercions do; instead they simply report ambiguous types if they can’t infer enough information. E.g.

fn main() {
    let x: i32 = 42;
    let y: i64 = x as _ as _;
}

simply cannot infer the intermediate type (could be e.g. either of i32 or i64)

error[E0282]: type annotations needed
 --> […location…]
  |
4 |     let y: i64 = x as _ as _;
  |                       ^ cannot infer type
  |
  = note: type must be known at this point

With this explicit as _ cast, all the remaining potential coercion sites are eliminated, and thus both the type of the Box::new(…) as well as the target type of the as _ conversion can be inferred, the cast is checked and turns out to be a valid cast (since all implicit coercions are also valid casts).

3 Likes

After second-guessing/pondering, I would be surprised if Client { user: Arc::new(Mutex::new(Box::new(GuestUser {}))) } can compiles too, as Rust has no implicit casting/coercion. As far as I know, implicit casting on primitive types like let x: i32 = 42; let y: i64 = x; is not allowed. Type inference is the most "implicit" way we could have.

But then, again, why Client { user: Box::new(GuestUser {})) }, implicit coercion from Box<GuestUser> to Box<dyn User> is okay? I suppose (correct if I'm wrong) it is one the of ergonomic features that Rust provides, which confuse newbies like me sometimes at the expense of concision. Wouldn't it be better if we forced it to be explicit? Or at least make it configurable in the .toml file?

PS: I was hoping for a detailed explanation, but this exceeded my expectation. Thanks for spending so much time answering my question.
PPS: I'm still new to Rust, there are a lot of things for me to take in. Sorry if I said anything stupid.

I mean, in case I wasn’t clear: it does compile… which is kind-of what motivated me to come up with a potential explanation as-to why it compiles in my answer above, in the first place.

Yeah, well… it does have implicit conversions, just no implicit conversions between integer types. See here for a complete list, but the most common ones are probably

  • subtyping coercions, i.e. the thing that “(co-)variance” allows, e.g. coercing between references of different lifetime (like &'a T to &'b T where 'a: 'b).
  • deref coercions, e.g. &String to &str, which allows coercing references using the Deref/DerefMut trait
  • unsizing coercions, creating trait objects or slices
    • e.g. from &[T; N] to &[T]
    • or the example we’re looking at here, from Box<GuestUser> into Box<dyn User>
  • [there’s also &mut T to &T (and a few similar ones to or between raw pointers), and a few more coercions; as mentioned above, I’ve linked the full list if you’re interested]

W.r.t. problems in combination with type inference, note that (most[1]) subtyping coercions are kind-of special because they don’t get eliminated. Coercing between &'a T and &'b T will always work even with generic functions and lots of inference, essentially because it’s okay for lifetimes to be under-specified; there’s no analogue of the “type annotations needed” error for lifetimes.

In the context of the model of type-inference I specified above, I would guess that a way to account for this fact is by, instead of saying “both types need to be exactly equal” whenever we choose to eliminate one of the coercion sites, we’d instead say “both types need to be equal, except for differences in lifetimes”, which is a strong enough insight in order to be able to make progress with type inference just as well, but you do end up with a lot of (potentially) different lifetime parameters this way. As noted above, under-constrained lifetime parameters are not a problem; you (being the compiler) just write down all the necessary interrelations between those lifetimes that you need to hold for all the mostly-eliminated-but-still(-lifetime-only/subtyping)-coercions to be valid, and then give that to the borrow checker to figure out if that system of lifetimes is solvable.


  1. I haven’t tested it, but I’d assume that certain kinds of subtyping coercions, namely those that do involve more than just a change in the lifetime, don’t have this “superpower”; the main case that comes to mind is something like coercing a higher-ranked type for<'a> fn(&'a ()) into a concrete fn(&'b ()) for a single lifetime 'b; which is technically also a “subtyping coercion” ↩︎

1 Like

I just learned what you meant by coercion sites, but still too much to take in. Maybe I shouldn't worry about those intricacies at my current stage, it will slowly make sense to me as I go.

Again, thanks for your patience in explaining these to me.

In case that wasn't clear: you can of course ignore the part of my answer(s) that goes into detail how type inference might operate; to a large degree, I also wrote that part for myself to see if the set of rules I came up with would work for the examples at hand. The most relevant part is

perhaps with the additional clarification that the way that type inference breaks if coercions aren't eliminated (by some heuristic at some point) is that all the intermediate types are going to be under-specified / ambiguous if you'd allow coercions in so many places; similar to how it is with the x as _ as _ example in my latest answer behaves.

2 Likes

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.