Is changing the return type of a function to a subtype a breaking change?

Imagine I have the following code:

pub struct Foo<'a>(pub &'a str);
pub struct Bar<'a>(Foo<'a>);
impl Bar<'_> {
    pub const fn foo(&self) -> &Foo<'_> {
        &self.0
    }
}

I would like to change that to:

pub struct Foo<'a>(pub &'a str);
pub struct Bar<'a>(Foo<'a>);
impl<'a> Bar<'a> {
    pub const fn foo(&self) -> &Foo<'a> {
        &self.0
    }
}

Is that change a "breaking" change (i.e., must I increment the SemVer major version number)?

I don't think it is. According to SemVer Compatibility, "loosening generic bounds" is a minor change which I think this constitutes as. I just want to make sure though.

Edit

I realize there are a lot of important details that will impact the answer to this question, so the actual code I'm referring to is webauthn_rp::AuthenticationClientState::options. This is one of the downsides of lifetime elision: if one is not careful, they can shoot themselves in the foot and end up with stricter bounds.

I agree. You have to be sure the lifetimes are covariant (which I believe they are here).

I could maybe cook up some niche circumstances with higher ranked types or such, but that's not the case here and would probably only amount to inference breakage vs a major breaking change.

Yeah, when you're sharing via fields that are shared borrows, elision is often not what you want.

2 Likes

Thanks. Additionally, the types don't implement Any unless the lifetimes are 'static; thus "reflection-like code" shouldn't exist either. While I probably won't let such a "niche" example prevent me from making this a minor version bump, I'm still interested in such an example if for no other reason than the opportunity to learn.

Change only one of fn a or fn b to return fn(&str) in this playground.

An inference based example.

1 Like