rustwrapperinvariance

How do I deal with wrapper type invariance in Rust?


References to wrapper types like &Rc<T> and &Box<T> are invariant in T (&Rc<T> is not a &Rc<U> even if T is a U). A concrete example of the issue (Rust Playground):

use std::rc::Rc;
use std::rc::Weak;

trait MyTrait {}

struct MyStruct {
}

impl MyTrait for MyStruct {}

fn foo(rc_trait: Weak<MyTrait>) {}

fn main() {
    let a = Rc::new(MyStruct {});
    foo(Rc::downgrade(&a));
}

This code results in the following error:

<anon>:15:23: 15:25 error: mismatched types:
 expected `&alloc::rc::Rc<MyTrait>`,
    found `&alloc::rc::Rc<MyStruct>`

Similar example (with similar error) with Box<T> (Rust Playground):

trait MyTrait {}

struct MyStruct {
}

impl MyTrait for MyStruct {}

fn foo(rc_trait: &Box<MyTrait>) {}

fn main() {
    let a = Box::new(MyStruct {});
    foo(&a);
}

In these cases I could of course just annotate a with the desired type, but in many cases that won't be possible because the original type is needed as well. So what do I do then?


Solution

  • What you see here is not related to variance and subtyping at all.

    First, the most informative read on subtyping in Rust is this chapter of Nomicon. You can find there that in Rust subtyping relationship (i.e. when you can pass a value of one type to a function or a variable which expects a variable of different type) is very limited. It can only be observed when you're working with lifetimes.

    For example, the following piece of code shows how exactly &Box<T> is (co)variant:

    fn test<'a>(x: &'a Box<&'a i32>) {}
    
    fn main() {
        static X: i32 = 12;
        let xr: &'static i32 = &X;
        let xb: Box<&'static i32> = Box::new(xr);  // <---- start of box lifetime
        let xbr: &Box<&'static i32> = &xb;
        test(xbr);  // Covariance in action: since 'static is longer than or the 
                    // same as any 'a, &Box<&'static i32> can be passed to
                    // a function which expects &'a Box<&'a i32>
                    //
                    // Note that it is important that both "inner" and "outer"
                    // references in the function signature are defined with
                    // the same lifetime parameter, and thus in `test(xbr)` call
                    // 'a gets instantiated with the lifetime associated with
                    // the scope I've marked with <----, but nevertheless we are
                    // able to pass &'static i32 as &'a i32 because the
                    // aforementioned scope is less than 'static, therefore any
                    // shared reference type with 'static lifetime is a subtype of
                    // a reference type with the lifetime of that scope
    }  // <---- end of box lifetime
    

    This program compiles, which means that both & and Box are covariant over their respective type and lifetime parameters.

    Unlike most of "conventional" OOP languages which have classes/interfaces like C++ and Java, in Rust traits do not introduce subtyping relationship. Even though, say,

    trait Show {
        fn show(&self) -> String;
    }
    

    highly resembles

    interface Show {
        String show();
    }
    

    in some language like Java, they are quite different in semantics. In Rust bare trait, when used as a type, is never a supertype of any type which implements this trait:

    impl Show for i32 { ... }
    
    // the above does not mean that i32 <: Show
    

    Show, while being a trait, indeed can be used in type position, but it denotes a special unsized type which can only be used to form trait objects. You cannot have values of the bare trait type, therefore it does not even make sense to talk about subtyping and variance with bare trait types.

    Trait objects take form of &SomeTrait or &mut SomeTrait or SmartPointer<SomeTrait>, and they can be passed around and stored in variables and they are needed to abstract away the actual implementation of the trait. However, &T where T: SomeTrait is not a subtype of &SomeTrait, and these types do not participate in variance at all.

    Trait objects and regular pointers have incompatible internal structure: &T is just a regular pointer to a concrete type T, while &SomeTrait is a fat pointer which contains a pointer to the original value of a type which implements SomeTrait and also a second pointer to a vtable for the implementation of SomeTrait of the aforementioned type.

    The fact that passing &T as &SomeTrait or Rc<T> as Rc<SomeTrait> works happens because Rust does automatic coercion for references and smart pointers: it is able to construct a fat pointer &SomeTrait for a regular reference &T if it knows T; this is quite natural, I believe. For instance, your example with Rc::downgrade() works because Rc::downgrade() returns a value of type Weak<MyStruct> which gets coerced to Weak<MyTrait>.

    However, constructing &Box<SomeTrait> out of &Box<T> if T: SomeTrait is much more complex: for one, the compiler would need to allocate a new temporary value because Box<T> and Box<SomeTrait> has different memory representations. If you have, say, Box<Box<T>>, getting Box<Box<SomeTrait>> out of it is even more complex, because it would need creating a new allocation on the heap to store Box<SomeTrait>. Thus, there are no automatic coercions for nested references and smart pointers, and again, this is not connected with subtyping and variance at all.