Why Option<&mut MyStruct> is not casted automatically to Option<&MyStruct>?


#1

Having

fn my_func(x: Option<&MyStruct>) -> ()

the following does not compile

let x: Option<&mut MyStruct> = None; // or what ever right value assignment
my_func(x);

with an error:

types differ in mutability
    = note: expected type `std::option::Option<&MyStruct>`
               found type `std::option::Option<&mut MyStruct>`

Why? I would expect it is safe implicit cast to a type with stronger guarantees…

What is the most elegant way to convert a variable of Option<&mut MyStruct> to Option<&MyStruct>?


#2

The shortest way that comes to mind is .map(|x| &*x).

(I thought just |x| x originally, but apparently inference isn’t smart enough for that).


#3

Do you have control over my_func()? If so, you can generify it as fn my_func<A: std::borrow::Borrow<MyStruct>>(a: Option<A>). That will let you pass a whole bunch of types inside the option, including a mutable ref.

If you’re in a Try context, you can also do Some(x?). Otherwise, what @scottmcm suggested.


#4

Because

fn my_func(x: Option<&MyStruct>) -> ()

You should do this instead

fn my_func(x: Option<&mut MyStruct>) -> ()

#5

Why? The function does not require mutable guarantee.


#6

Is it the standard pattern to solve this sort of a problem? Could you please explain what happens here? How can MyStruct or &mut MyStruct become equal to borrow?


#7

Because

impl<'a, T> Borrow<T> for &'a T

and

impl<'a, T> Borrow<T> for &'a mut T

(From Borrow doc page)

So Borrow can be used to abstract a type over the concrete type, and shared and mut references. But also if it is in a Box, or an Rc. Look at the list of the impl in the doc :wink:


#8

Very nice. Thanks. I have read the book but effective rust chapter is either new or I need to reread it again after I gained some hands on experience with rust :slight_smile:


#9

@ndusart already explained why it works so I won’t repeat that. But yeah, this is a common approach to give yourself (or your users) flexibility and ergonomics in calling functions that don’t care about ownership of the arguments.

I’ll also note that AsRef can be used as well but it has fewer blanket impls than Borrow; Borrow has additional impl requirements in the case of Self being Eq, Hash, or Ord - that’s something to keep in mind. AsRef has none of that.


#10

To answer the “why isn’t it casted automatically” question: because in general, the compiler can’t reason about whether such a cast matches the semantics of a type (in this case, Option). E.g. it is most definitely not okay to convert Sender<&mut T> to Sender<&T> (otherwise it’d be possible to send a shared reference and receive a “unique” one from the corresponding Receiver).

This property of generic types in relation to subtyping relations between the types they’re generic over is called ‘variance’, and in fact, Rust has some support for variance (although there are some complications compared to most other languages). However, subtyping in Rust only “works” for nested lifetimes.

If unique and shared references (plus, maybe, raw pointers) are guaranteed to have the same ABI, it would make sense to extend the subtyping relations to them, too.


#11

I am familiar with variance of generics. It is well defined in Scala. And custom user generic type defines variance in it’s declaration. In rust it is a bit different.

First, variance for a custom type is inferred automatically depending on how generic type arguments are used (see example in the chapter you referred to in the previous post). This is a bit hard to figure out for users of a type without knowing implementation / usage details, which seems not a good sign. Transparency and visibility of this could be improved somehow?

Secondly, it seems mutability sometimes influences covariance. For example: the docs claim that Box is covariant over it’s type argument. It means if T: U (variable of type T is assignable to variable of type U), so Box<T>: Box<U> (variable of type Box<T> is assignable to variable of type Box<U>), but it is no longer true when mut is introduced and assignment happens through different paths. For example (full play code is here):

struct MyS { ... }
trait MyT { ... }
impl MyT for MyS { ... }

fn call_value(a: &MyT) { ... }
fn call_box(a: Box<&MyT>) { ... }
fn call_option(a: Option<&MyT>) { ... }

// the following works:
// I read it as: mutable reference is "subtype" of immutable refernce
// "muttable variable is assignable to immutable variable of the same type"
    let mut mys_mut = MyS{a: 1};
    let myt_mut: &mut MyT = mys_mut.as_trait_mut(); // returns self
    let value_myt_mut: &mut MyT = myt_mut;
    let value_myt: &MyT = value_myt_mut; // <----- assignment works
    call_value(value_myt); // <---- and variable leaks to a function as immutable

// the following works as expected for Box when things are immutable
// "Box variable of a struct is assignable to Box variable of a trait, if struct implements this trait"
    let mys: Box<MyS> = Box::new(MyS{a: 1});
    let mut myt: Box<MyT> = Box::new(MyS{a: 1});
    myt = mys; // <----- assignment works: MyS is MyT, so Box of MyS is also Box of MyT
    
// the following works, although some things are mutable now
// "Box variable of mutable reference is assignable to Box variable of immutable reference of the same type"
    let mut mys_mut = MyS{a: 1};
    let myt_mut: &mut MyT = mys_mut.as_trait_mut();
    let box_myt: Box<&MyT> = Box::new(myt_mut); // <----- assignment works
    call_box(box_myt); // <---- and Box<&T> variable formed from Box<&mut T> leaks to a function

// however, big surprise, the following does not work... WHY?
// although it is same (or different?) to the previous case
// this effectively suggest that Box is NOT really covariant as docs describe it
// or what I am missing?
    let mut mys_mut = MyS{a: 1};
    let myt_mut: &mut MyT = mys_mut.as_trait_mut();
    // let box_myt_mut = Box::new(myt_mut);
    // let box_myt: Box<&MyT> = box_myt_mut;  // <----- assignment does not work

// the same behavior is for Option:
    let mut mys_mut = MyS{a: 1};
    let myt_mut: &mut MyT = mys_mut.as_trait_mut();
//works
    let maybe_myt: Option<&MyT> = Some(myt_mut);
// does not work
    // let maybe_myt_mut = Some(myt_mut);
    // let maybe_myt: Option<&MyT> = maybe_myt_mut;
    call_option(maybe_myt);  // <---- and Option<&T> variable formed from Option<&mut T> leaks to a function

So, why Box<U> is sometimes assignable and sometimes NOT assignable from Box<T>, when U is definitely always assignable from T? When Box is not always assignable, why docs suggest that Box is always covariant?

Same question for Option: why Option<&T> is sometimes assignable and sometimes NOT assignable from Option<&mut T>, when &T is always assignable from &mut T? Is it covariant or not, documentation does not tell, and I find it hard to conclude myself.


#12

There’s no subtyping going on here, as far as I can see. Instead, you’re seeing coercions occurring, particularly unsizing ones:

Option is not eligible for coercions because (in short) it’s not a smart pointer and doesn’t unsize to anything.


#13

Right. I incorrectly assumed there was sub-typing. It is seems it is time to read nomicon :slight_smile: Thanks!


#14

In Scala and most other languages that care about variance, objects live on the heap and the only thing “casting” does is telling the complier what methods can be called on a given pointer.

In Rust it’s not that simple: &T and &Trait have different size (“fat pointers”), so Rust has to do additional transformations when you’re casting &T to &Trait on the stack (you don’t have to write special take methods for that, btw, just a cast/assignment works), and you can’t just cast/assign Foo<&T> to Foo<&Trait> because that’d require rewriting the object (or rather creating a new one, because it may have a different size…) to change all the references to T it has.

This is why, variance-wise, &T is not a subtype of &Trait. Instead, there is some special unstable unsizing magic in a form of a few traits that lets the type such as Box to opt-in into this “recreate with internal references swapped for fat references” coercion (in its current form it only supports having one such reference).

The same can be said about other conversions/coercions, e.g. you can cast 35u32 as u64, but you can’t just treat Foo<u32> as Foo<u64>, although I doubt there will ever be any traits or language support to enable that.

So, back to variance, the only subtyping relation in Rust is 'a: 'b and Foo<'c>: Foo<'d>, because lifetimes don’t influence the ABI. If the ABI is guaranteed to be the same for unique and shared references to sized types, the subtyping relations could be extended to allow casting from Foo<&mut T> to Foo<&T> if Foo is covariant (anyone feels like writing an RFC?).

P.S. not sure this is relevant for your explorations, but &mut T is only covariant over T if T: !Sized (because then you can’t “write” another object through it, just modify the existing one), otherwise it’s invariant.


#15

I could write, although I am not sure what is involved in the process.