Skip to content
This repository has been archived by the owner on Apr 5, 2024. It is now read-only.

Check for variance changes #364

Open
mstange opened this issue Dec 3, 2022 · 1 comment
Open

Check for variance changes #364

mstange opened this issue Dec 3, 2022 · 1 comment

Comments

@mstange
Copy link

mstange commented Dec 3, 2022

I just learned about variance in Rust, and was struck by its potential to cause unintended API breakages.

Does this tool check for changes in a type's variance over its lifetime and type parameters?

For example, a generic struct of the form TheStruct<T> can go from being "covariant in T" to being "invariant in T" simply because a hidden field was added to the struct. Same with lifetime parameters: AnotherStruct<'a> can go from being "covariant in 'a" to being "invariant in 'a".

From https://doc.rust-lang.org/reference/subtyping.html:

The variance of other struct, enum, and union types is decided by looking at the variance of the types of their fields.

Variance is an aspect of a type's API that is not immediately obvious, yet it has the potential to break existing code when it changes.

Here's an example where crate_1_0_0::TheStruct is covariant in T, which allows the function cast_return_value_1_0_0 to compile successfully. But in crate_1_0_1, TheStruct now has a Mutex field which causes TheStruct to become invariant in T, and as a result, cast_return_value_1_0_1 no longer compiles.

mod crate_1_0_0 {
    pub struct TheStruct<T> {
        inner: T,
    }
    impl<T> TheStruct<T> {
        pub fn new(inner: T) -> Self {
            Self { inner }
        }
        pub fn into_inner(self) -> T {
            self.inner
        }
    }
}

mod crate_1_0_1 {
    pub struct TheStruct<T> {
        inner: std::sync::Mutex<T>,
    }
    impl<T> TheStruct<T> {
        pub fn new(inner: T) -> Self {
            Self {
                inner: std::sync::Mutex::new(inner),
            }
        }
        pub fn into_inner(self) -> T {
            self.inner.into_inner().unwrap()
        }
    }
}

// Compiles successfully
fn cast_return_value_1_0_0<'a>(
    x: crate_1_0_0::TheStruct<&'a str>,
    y: crate_1_0_0::TheStruct<&'static str>,
) -> crate_1_0_0::TheStruct<&'a str> {
    println!("Discarding: {}", x.into_inner());
    y
}

// Does not compile
fn cast_return_value_1_0_1<'a>(
    x: crate_1_0_1::TheStruct<&'a str>,
    y: crate_1_0_1::TheStruct<&'static str>,
) -> crate_1_0_1::TheStruct<&'a str> {
    println!("Discarding: {}", x.into_inner());
    y
}

fn main() {
    let s = String::from("Short-lived string");
    let struct_short_lived = crate_1_0_0::TheStruct::new(s.as_str());
    let struct_static = crate_1_0_0::TheStruct::new("Static string");
    let one = cast_return_value_1_0_0(struct_short_lived, struct_static);
    println!("Remaining: {}", one.into_inner());

    let s = String::from("Short-lived string");
    let struct_short_lived = crate_1_0_1::TheStruct::new(s.as_str());
    let struct_static = crate_1_0_1::TheStruct::new("Static string");
    let one = cast_return_value_1_0_1(struct_short_lived, struct_static);
    println!("Remaining: {}", one.into_inner());
}

This prints:

error: lifetime may not live long enough
  --> src/main.rs:46:5
   |
41 | fn cast_return_value_1_0_1<'a>(
   |                            -- lifetime `'a` defined here
...
46 |     y
   |     ^ returning this value requires that `'a` must outlive `'static`
   |
   = note: requirement occurs because of the type `crate_1_0_1::TheStruct<&str>`, which makes the generic argument `&str` invariant
   = note: the struct `crate_1_0_1::TheStruct<T>` is invariant over the parameter `T`
   = help: see <https://doc.rust-lang.org/nomicon/subtyping.html> for more information about variance
@mstange
Copy link
Author

mstange commented Dec 3, 2022

In practice, one easy way to depend on a type's variance is when using self_cell or yoke to make self-referential types. These crates require the "dependent" type to be covariant in its lifetime parameter. So if your dependent type is struct Dependent<'a> { parsed_object: object::read::File<'a>, }, you're now at the mercy of the object::read::File type and your code will break if the object::File type changes to be no longer covariant in its lifetime parameter.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant