Rule(s) about casting from trait implementation type to dyn trait type

Hello everyone.

I apologize in advance if topic name is misleading, but don't know how in short describe the situation.

problem:

Need help to understand why in some cases one must(?) do an explicit cast from Trait implementation to dyn Trait and in some not, however they look very similar.

Given trait and impl:

trait Foo {}
struct MyFoo {}
impl Foo for MyFoo {}

This does not compile:

fn returns_bad() -> Result<Box<dyn Foo>, u32> {
    Ok(MyFoo {})
        .map(|ch| Box::new(ch)); 
}

the error is:

= note: expected enum `Result<Box<(dyn Foo + 'static)>, u32>`
           found enum `Result<Box<MyFoo>, _>`

But this compiles without an error - with added explicit cast as Box<dyn Foo>:

fn returns_ok() -> Result<Box<dyn Foo>, u32> {
    Ok(MyFoo {})
        .map(|ch| Box::new(ch) as Box<dyn Foo>)
}

I'v tried juggling different combo to deduce the logic, but failed.

At the same time this works without an explicit cast:

fn produces_impl() -> Box<MyFoo> {
    Box::new(MyFoo {})
}

fn casts_impl() -> Box<dyn Foo> {
    produces_impl()
}

Don't understand why returns_bad does not compile as is.

Playground link with illustration

Thanks in advance.

Coercions, to Box<dyn Foo> in this case, only happen at coercion sites. That's why it is coerced when it is the function result.

A let with a type annotation is also a coercion site, so this works:

fn returns_bad() -> Result<Box<dyn Foo>, u32> {
    let dyn_foo: Box<dyn Foo> = Box::new(MyFoo {});
    Ok(dyn_foo)
}

EDIT: It turns out that it is not a lack of coercion sites as explained in the following posts. @solc42, I suggest changing the accepted answer to the post from @quinedot or @kpreid, so that people reading this are not confused.

Thank you!

1 Like

Incidentally, when you need to explicitly coerce, it is often enough to just signal that a coercion is required (and let the compiler infer the target type).

 fn returns_ok() -> Result<Box<dyn Foo>, u32> {
-    Ok(MyFoo {}).map(|ch| Box::new(ch) as Box<dyn Foo>)
+    Ok(MyFoo {}).map(|ch| Box::new(ch) as _)
 }
4 Likes

Lack of coercion sites isn't the problem here; in the code

fn returns_bad() -> Result<Box<dyn Foo>, u32> {
    Ok(MyFoo {})
        .map(|ch| Box::new(ch))
}

there are two relevant coercion sites (one in the map closure, and one for the whole function). The problem is that

  • coercion sites are only made use of when mandatory (a type mismatch is observed), and
  • you can't use an unsizing coercion on a Result, because it doesn't implement CoerceUnsized.

So what happens is, the expression Ok(MyFoo {}).map(|ch| Box::new(ch)) successfully constructs a Result<Box<MyFoo>, ?> without doing any coercion. Then the compiler gets to considering the return value of the outer returns_bad function, and sees a mismatch between the two types

Result<Box<MyFoo>, ?>
Result<Box<dyn Foo>, u32>

which it can't resolve via a coercion, because there is no impl CoerceUnsized for Result. And it won't currently work backwards to change its mind about what type the map() should produce.

Inserting as works in this case not because it inserts a coercion site that wasn't previously available, but because it changes how type information propagates:

  • Adding Box::new(ch) as Box<dyn Foo> forces the map return type to be Box<dyn Foo>, so the coercion must happen there.
  • Adding Box::new(ch) as _ tells the compiler that some sort of conversion should happen here, so it stops taking the type of Box::new(ch) as information about what type the produced Result should have. Then it can get the correct type from the enclosing code and fill that in the _ spot.
5 Likes

Wow, things are even more tricky!

Thank you all for your time and sharing the knowledge: @kpreid, @quinedot, @jumpnbrownweasel

PS @jumpnbrownweasel Marked detailed of @kpreid as solution as you suggested, but I do appreciate your answer cause it also gave me an explicit hint to rust documentation about coercion sites!

1 Like