2

After struggling for hours with writing a factory method which returns instances of a struct containing a field of a generic type (implementing a trait), I find that it compiles and runs only if I do not assign the result to a variable before returning it (see the bottom of the post for a complete working example):

This compiles and runs:

fn compiles() -> Parent<dyn IsAChild> {
    return match rand::thread_rng().gen_bool(1.0/2.0) {
        true => Parent { child: Box::new(Daughter{} ) },
        false => Parent { child: Box::new(Son{} ) },
    }
}

This does not compile:

fn does_not_compile() -> Parent<dyn IsAChild> {
    return match rand::thread_rng().gen_bool(1.0/2.0) {
        true => {
            let parent = Parent { child: Box::new(Daughter {}) };
            parent
        },
        false => {
            let parent = Parent { child: Box::new(Son {}) };
            parent
        },
    }

}

The does_not_compile() function results in the following error:

error[E0308]: mismatched types
  --> src\main.rs:39:13
   |
39 |             parent
   |             ^^^^^^ expected trait object `dyn IsAChild`, found struct `Daughter`
   |
   = note: expected struct `Parent<(dyn IsAChild + 'static)>`
              found struct `Parent<Daughter>`

This has me completely stumped. To me there is no semantic difference between the two, only the fact that the result is (temporarily) stored in a variable before it is returned. What is going on here?

Complete example (add rand = "*" to the dependencies):

use rand::Rng;

trait IsAChild {}

struct Parent<T: IsAChild + ?Sized> {
    child: Box<T>
}

struct Son;
impl IsAChild for Son {}

struct Daughter;
impl IsAChild for Daughter {}

fn compiles() -> Parent<dyn IsAChild> {
    return match rand::thread_rng().gen_bool(1.0/2.0) {
        true => Parent { child: Box::new(Daughter{} ) },
        false => Parent { child: Box::new(Son{} ) },
    }
}

fn compiles_too() -> Parent<dyn IsAChild> {
    return match rand::thread_rng().gen_bool(1.0/2.0) {
        true => {
            println!("It's a girl!");
            Parent { child: Box::new(Daughter{} ) }
        },
        false => {
            println!("It's a boy!");
            Parent { child: Box::new(Son{} ) }
        },
    }
}

/*
fn does_not_compile() -> Parent<dyn IsAChild> {
    return match rand::thread_rng().gen_bool(1.0/2.0) {
        true => {
            let parent = Parent { child: Box::new(Daughter {}) };
            parent
        },
        false => {
            let parent = Parent { child: Box::new(Son {}) };
            parent
        },
    }
}
*/

fn main() {
    compiles();
}    
Dag Sondre Hansen
  • 2,449
  • 20
  • 22

2 Answers2

3

The difference here is type inference. In the first example because the expression is the return value, the compiler looks to the function signature to determine the generic parameter of Parent, which it decides is dyn IsAChild. In the second example, the compiler needs to infer the type of the variable parent. It first looks at the type of the expression it is assigned to, and the type of Parent { child: Box::new(Daughter {}) } is unambiguously Parent<Daughter>, so the variable is given that type instead of Parent<dyn IsAChild>. You could fix th second example by explicitly saying

let parent: Parent<dyn IsAChild> = Parent { child: Box::new(Daughter {}) };

The compiler only coerces the contents of the Box to an unsized type if it has to, so that's why the example with the variable needs to be explicit.

Ian S.
  • 1,831
  • 9
  • 17
  • It looks like this is vaguely related to [this other question](https://stackoverflow.com/questions/72246105/why-does-stdfswrite-use-an-inner-function), which has an interesting (if brief) description on how adding a concrete type changes the compiler's behavior. – Jeremy Meadows Jun 02 '22 at 20:49
  • @JeremyMeadows How is that related? – Chayim Friedman Jun 02 '22 at 21:00
  • @ChayimFriedman both examples show how Rust can reduce an object of multiple types (whether that's behind an `AsRef` or as a `dyn`) into exactly one type. I get that they function pretty differently under the hood, but coming from some Java background and fighting with dispatch stuff, it seems pretty interesting to me how Rust handles that so differently. Sorry if I'm misunderstanding something, though. – Jeremy Meadows Jun 02 '22 at 21:22
  • 1
    I think this explanation is a bit incomplete. The return value of a function is a coercion site, so unsized coercions happen at that site regardless of whether the returned value is an expression or a variable. If you return a trait object directly, it doesn't matter whether you first assign it to a variable or not. The problem here is that while there is an unsized coercion from `Box` to `Box`, there is no unsized coercion from `Parent` to `Parent`. In the working example, the coercion site is the value of `child`, not the whole return value. – Sven Marnach Jun 02 '22 at 21:24
  • See [this playground example](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=40c5da80abea58feab3a7271839d62e9) for an illustration. The return value is assigned to a variable with the wrong type, but it gets coerced to th right type anyway. – Sven Marnach Jun 02 '22 at 21:28
1

It's because the compiler deduces the exact type of the crated Parent from the usage we make of it.

When this is directly the result of the match statement, thus the result of the function, then this type must match exactly Parent<dyn IsAChild>.

But if you store it in a local variable, the type is not constrained and it's simply Parent<Daughter> or Parent<Son> (no dyn here). When these variables are used in order to produce the result of the function, they do not match.

You can force the type of the variable let parent: Parent<dyn IsAChild> = Parent { child: Box::new(Daughter {}), };.

prog-fh
  • 13,492
  • 1
  • 15
  • 30