Trait method call with if-else and closures

Hello! I've encountered the following compiler behavior which occurs when a trait method is called with a parameter being a conditional returning closures. For example:

trait Foo<I : Fn() -> i32>{
    fn foo(&self, param : I) -> (); //replace I with Box<dyn Fn() -> i32>
}
struct S {}

impl Foo<Box<dyn Fn() -> i32>> for S {
    fn foo(&self, param : Box<dyn Fn() -> i32>) -> () {
        unimplemented!();
    }
}

fn main() {
    let s = S {};
    // This function call does not compile
    s.foo(if true {
                Box::new(|| {42})} 
            else {
                Box::new(|| {42})
         });
}

The compiler error message says that if and else have incompatible types. While this could make sense as every closure in Rust has its own type, I'm wondering why changing param : I to param : Box<dyn Fn() -> i32> in foo in trait Foo declaration would make the code compile (while keeping the rest unchanged). Because then the compiler is able to infer the type of both closures to be trait object type. Also if we replace if-statement inside function call with just one closure, the code would compile for both cases.
Have you encountered something similar before? I know this might seem to be a bit cherry-picked case, but it got we wondering why the compiler behaves in this way.
Thank you

When you specify a trait object as the type, both closures will be coerced to that type. If you don't specify a concrete type, then the closures will be left alone and no coercion will happen – the param argument of foo is generic, after all, so why should any coercion happen?

Coercion where the common "supertype" is pulled out of thin air is a footgun and simply not realistic. Based on that logic, the following should compile, too:

let x /* : &dyn Display */ = if true {
    &0_i32
} else {
    &"foo"
};

Why shouldn't the compiler be able to guess that you meant dyn Display? The obvious answers are:

  1. it's magic, and magic is bad: it makes the code error-prone and hard to read
  2. it's simply impossible in this specific case due to ambiguity. In the above example, both types implement a whole bunch of traits that you could coerce to (e.g. Debug); the identical closures will also necessarily implement all the same traits. How should the compiler know that you meant dyn Fn and not dyn Send or dyn Clone (however nonsensical/impossible that may be) or dyn Whatever3rdPartyTraitIsImplementedOnClosures?
2 Likes

The compiler sees that a Box<dyn Fn() -> i32> is expected as an argument and this influences the compiler to coerce the if/else, and thus the individual arms. Whereas with the generic, the boxed closure type meets the expectation of the function argument without coercion.

If you coerce the if arm explicitly, the compiler will similarly realize it should coerce the else arm.

    s.foo(if true {
        Box::new(|| 42) as Box<dyn Fn() -> i32>
    } else {
        Box::new(|| 42)
    });

The type of the closure is not dyn Fn() -> i32. That is its own type, and it's not a supertype of the closure either, even though the closure can be unsize-coerced to dyn Fn() -> i32. All types, including closures, typically implement many traits, and the language doesn't have some "default unsizing coercion" based on generic bounds.

There are similar situations that you can run into that don't involve dyn Trait, such as references to arrays (versus slices).[1]


  1. Change the first arm to &[0][..] and it will compile. ↩ī¸Ž

2 Likes

Why is that the case? I thought that since param in the signature of foo in the impl block has specified type, it would just have that type and not be generic for the call on s?

Sorry, I though for a moment that the type parameter was on the method, but the reasoning is almost the same. The type parameter I must still be inferred from somewhere, and it still must be substituted with a concrete type. Since the closures have two different types, this substitution is yet again impossible (without a coercion, but complete inference and coercion are mutually exclusive for the aforementioned reasons).

1 Like

Why is that the case? I probably misunderstood how implementing traits works. I am not sure why the compiler needs to look at declararation of foo in the parameterized trait and deal with the type parameter, while it has method foo in the impl block.

Because the type I is used in the method. The method's argument is of type I, is it not?


(Even if it weren't, a method needs to be called on a concrete trait; generic "types" and "traits" aren't really types or traits, they are "type constructors" and "trait constructors", respectively. Your trait Foo isn't a trait; only Foo<T> is, for some specific type T. Again, the compiler can't just assume you meant Foo<dyn Fn>, for example because your struct S may implement any number of Foo<T> traits, for different choices of T.)

1 Like

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.