This idea seems like such a no-brainer, almost: The requirement for the dispatched argument of trait functions to be methods, i.e. taking a self
argument, seems to have no technical motivation. Maybe it just grew this way historically? I’m having a surprisingly hard time finding prior discussion on this, please point out (i.e. link) anything that I’ve missed.
What this is about
Currently, a trait like
trait Frobnicable {
fn frobnicate(self: &Self, arg: Information) -> Data;
}
is object safe, but a trait like
trait Frobnicable {
fn frobnicate(not_self: &Self, arg: Information) -> Data;
}
isn’t.
But we could easily make this second version of Frobnicable
considered object safe, too, as long as the type of the first argument still fulfills the same conditions as usual. Maybe it could even be so that any argument, not just the first, (but instead then, the only one that mentions Self
type) could be dispatched on.
Why care?
The status quo of needing to have the dispatched argument be the first, and a self
argument, isn’t technically limiting any functionality, so it’s not particularly limiting. Also, since the restrictions for the dispatched arguments are such that it’s a valid self
-type, so it can be a method, you most often want a method anyways. Nonetheless… sometimes, you don’t want your API in a trait to be a method (or at least prefer not to have it be one). Maybe you don’t want to come up with a name that isn’t too likely to conflict with other methods of implementing types or other traits. Sometimes, you don’t really intend for the method to be (often) called by users at all.
Here’s the example where I’ve wanted this… feel free to point out others you can think of, too 😉
E.g. it can be useful to create some additional API in your trait just to support object safety. For example something like
trait VisitStrings: {
fn for_each_str(&self, f: impl FnMut(&str));
}
can be made object safe by using a dyn FnMut
callback… but you might not want the dynamic dispatch when you don’t need it. You can do this nicely with something like
trait VisitStrings: DynVisitStrings {
fn for_each_str(&self, f: impl FnMut(&str))
where
Self: Sized;
}
trait DynVisitStrings {
// currently *has* to be a method
fn dyn_for_each_str(&self, f: &mut dyn FnMut(&str));
}
impl<T: VisitStrings> DynVisitStrings for T {
fn dyn_for_each_str(&self, f: &mut dyn FnMut(&str)) {
(self as &T).for_each_str(f)
}
}
impl VisitStrings for Box<dyn VisitStrings + '_> {
fn for_each_str(&self, mut f: impl FnMut(&str)) {
<dyn VisitStrings>::dyn_for_each_str(self, &mut f)
}
}
impl VisitStrings for &(dyn VisitStrings + '_) {
fn for_each_str(&self, mut f: impl FnMut(&str)) {
<dyn VisitStrings>::dyn_for_each_str(self, &mut f)
}
}
and users can work with &dyn VisitStrings
or Box<dyn VisitStrings>
(or other pointer types, if such impls are added) still via the normal for_each_str
method; dyn_for_each
is more of an implementation detail, and perhaps useful occasionally for other smart pointers, but mostly it’s a method you don’t want to encourage being used, and making it not a method could help with making that clear.
Also now that I’m thinking about this… sometimes you’d want to apply above trick to achieve object safety on a trait where the original generic function (i.e. the one in the role of for_each_str
above) wasn’t a method in the first place, in which case the restriction that you have to make the dyn_…
a method seems particularly unnecessary
Semver concerns
Making more traits object safe comes with a meta semver concern. It’s not a breaking change itself, but it changes what changes are breaking. Such changes are (as far as I’m aware) often accepted[1], but for object safety here it could be affecting too many users (that’s a guess; I have not measured this in any way).
The problem here is two-fold. On one hand, crates could offer traits not intended to be object safe, which become object safe with this change. Their intention might have been future compatibility, with the desire to potentially add (default implemented) non-object-safe methods in the future. We would break this future compatibility. On the other hand, existing crates might have a trait which until now is not object safe in both versions, but this change might make it object safe on one and not object safe in the other version. I.e. the crate now suddenly has breaking changes between semver-minor versions.
Addressing these concerns is possible. For example object safety can be made opt-in
#[object_safe] // <- actual syntax might turn out different
trait Foo {}
and old editions could infer object safety as they do now, new editions never make a trait object safe implicitly, and the #[object_safe]
opt-in is allowed in all cases allowed by the new extended set of supported method signatures in object safe traits.
(Discussion of a feature of explicit object safety annotations (in too much detail) should be mostly off-topic here. Regarding the semver concerns, I’m only interested in discussions about whether the semver concerns should be considered important in the first place, and possibly short descriptions of solutions, and/or links to other [maybe existing] discussions.)
especially if running into this meta-breakage seems unlikely ↩︎