Changing RPIT to concrete type: breaking change?

And why or why not?

eg

fn foo() -> impl Trait {
  Implementor
}

to

fn foo() -> Implementor {
  Implementor
}

In general, I would think that's not a breaking change. This is because you shouldn't be able to do anything with an impl Trait value other than what Trait lets you do. By going to a concrete type, you would be allowing callers to do more with the return value (by virtue of knowing it's full type), not less, so I wouldn't expect existing code to break.

An exception to this could be method resolution: if the concrete type had an inherent method with the same name as a method in Trait, and a different signature, that could break existing code. Or worse: the same signature but different behaviour. Then, the code compiles, but no longer does what you expect. I've had that happen to me, and it was absolutely no fun whatsoever to debug. I genuinely thought I was going insane before I worked that one out...

2 Likes

I'm not sure about this. Wouldn't that imply that adding a method to any type is a breaking change?

trait FooTrait {
  fn foo(&self);
}

impl<T> FooTrait for T {
  fn foo(&self) {
    eprintln!("foo");
  }
}

fn main() {
  let lib = library::LibraryType;
  lib.foo();
}

In this situation, adding a foo method to LibraryType would cause lib.foo() to call that method instead of the trait's implementation.

It's a minor breaking change, not semver breaking. (Most changes can cause some sort of minor breakage.)

I haven't thought of a way to make going from RPIT to concrete semver breaking. There are other various related surprises from giving the caller more information. But it's typically analogous to a type implementing more traits or becoming covariant or so on.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.