Why are Fn types not contravariant in their argument types?

With Rust 1.86, we got a new subtyping relationship between trait objects through Trait upcasting. Consider these traits and a struct implementing them:

trait Media {}

trait Anime: Media {}

struct Frieren;
impl Media for Frieren {}
impl Anime for Frieren {}

The subtyping relationship is dyn Anime <: dyn Media. With this, Box<T> is covariant in T and so Box<dyn Anime> <: Box<dyn Media>. An example of this:

let some_anime: Box<dyn Anime> = Box::new(Frieren);
let some_media: Box<dyn Media> = some_anime; // this works

Box is covariant as I expected, but I also expected functions to be contravariant in their argument types. Consider these examples:

fn watch_media(media: Box<dyn Media>) {
    println!("Watching media");
}

fn watch_anime(anime: Box<dyn Anime>) {
    println!("Watching anime");
}

fn sit_down(watcher: impl Fn(Box<dyn Anime>)) {
    watcher(Box::new(Frieren));
}

With this, sit_down(watch_anime); works, but sit_down(watch_media); does not:

9  | fn watch_media(media: Box<dyn Media>) {
   | ------------------------------------- found signature defined here
...
23 |     sit_down(watch_media); // this does not work
   |     -------- ^^^^^^^^^^^ expected due to this
   |     |
   |     required by a bound introduced by this call
   |
   = note: expected function signature `fn(Box<(dyn Anime + 'static)>) -> _`
              found function signature `fn(Box<(dyn Media + 'static)>) -> _`

See the complete example on rust playground.

Logically, watch_media handles any type that implements Media, so it should be able to handle a type that implements Anime. But this is not allowed. I wanted to know why this isn't allowed and if it's even possible to have contravariance in Rust in terms of supertraits (not lifetimes).

I wasn't able to find any issues related to this in the rust GitHub or any discussion here. Please let me know if I missed it.

As an example of a language where this is allowed, see the same example in python.

A note on the workaround

I know there's a way to get this working by using this instead (the complete error message even hints to it)

sit_down(|anime| watch_media(anime));

This simply uses trait upcasting to get the type we need. But my question is broader: why isn't this made more implicit by making certain types contravariant (in this case, Fn)?

1 Like

In Python, all values have a uniform representation*, and distinguishing between subtypes is always done at run time. The same is not true in Rust; though a Box<dyn Anime> can be converted into a Box<dyn Media>, they are represented differently. (This would be more obvious if Media had its own methods, like fn title(&self). In that case, watch_media might expect to be able to access the title method directly, but the representation of Box<dyn Anime> would only provide it behind a second layer of indirection. You can learn more about the representation of trait objects in Trait object types - The Rust Reference )

Now, the compiler could actually go a step further and insert this conversion for you! However, it can only cheaply do so in cases like this, where the function being passed is concrete. If you’re instead forwarding from one impl Fn to another, the conversion layer would have to capture the original function like a closure does, which would shorten its lifetime. So, at least for today, the language requires you to write the closure explicitly, giving a home for the code that does the upcasting.

* This is a polite fiction, the value representation for most Python interpreters is more like a carefully-constructed union or checked enum. But it’s still true that distinguishing types is done at run time.

3 Likes

There's currently no such subtyping relationship, instead there is a coercion between them, i.e. an implicit conversion (that's close if not a noop in certain cases). Note that the only subtyping relations in Rust are those involving lifetimes.

In addition to this, traits are currently always invariant with respect to their generic arguments, so even if there was subtyping relation between your Media and Anime trait objects, there would be no subtyping relation between Fn types over them.

7 Likes

From the RFC:

Note that this is a coercion and not a subtyping rule. That is observable because it means, for example, that Vec<Box<dyn Trait>> cannot be upcast to Vec<Box<dyn Supertrait>>. Coercion is required because vtable cocercion, in general, requires changes to the vtable, as described in the vtable layout section that comes next.

The lack of ability to infer variance on trait parameters was removed during the run-up to 1.0. Parameters of dyn _ types being invariant is mentioned at least in passing in the reference. There's no special carve out for the dyn Fn types.

6 Likes

Thanks for all the answers! This clarifies the initial misunderstanding that lead to the question. I had assumed that 1.86 introduced a new subtyping relationship between traits. This was further strengthened by the fact that Box appeared to be covariant.

For completeness, the behaviour I saw with Box was not covariance. It was just coercion. As an example, this is a similar kind of coercion as in function pointers being converted to trait objects of Fn:

let a: Box<fn(u32)> = Box::new(|x: u32| {});
let b: Box<dyn Fn(u32)> = a;
2 Likes