Minimum dyn trait subtyping (single inheritance)

Just tossing this out here in case anyone wants to tackle it.

It should be very easy to make dyn SubTrait be a subtype of dyn SuperTrait where SuperTrait is the only non-marker trait that SubTrait extends (it single-inherits from it, ignoring markers).

When this happens we can layout the vtable of SubTrait so that the SuperTrait vtable is a prefix of it, meaning no runtime coercions are required. I wouldn’t be surprised if this already worked by accident, since that’s the obvious way to layout vtables anyway.

I have been told that previous attempts at trait subtyping were always more ambitious than this and subsequently got bogged down in details. But this should be completely compatible with any future extensions without too much work.

It does lock us into guaranteeing that property of vtable layout but that seems fine?

10 Likes

FAQ

What about dyn A + B?

No.

What about trait Sub: Super1 + Super2?

No.

What about coercions?

No.

What about –

No.

Simple. Minimal. Compatible. Ship.

14 Likes

I am honestly okay with runtime conversions between Sub and Super for multiple supertraits.

I think. maybe through Into or something tho.

Ah, a fellow student of the school of get-shit-done-ism. ++ to all this.

Subtyping is maybe a step too controversial as it greatly extends the scope of subtyping in Rust (currently it’s only about lifetimes). Coercions, or “upcasting” of trait object pointers, would be a more minimal first step that would allow a lot of things, just not reinterpretation of e.g. &[&dyn SubTrait] as &[&dyn SuperTrait]. (This was discussed in discord, bringing it here for completeness.)

10 Likes

I don’t think it works as written since it means that adding an extra supertrait is a compatibility break.

Making it work for the first supertrait even if there are multiple should fix that.

2 Likes

Adding an extra supertrait is already a compatibility break since there’s no reason to expect all existing implementors to conform to that supertrait

2 Likes

Not for traits that cannot be implemented outside the crate (e.g. due to a private supertrait).

Also I think a blanket impl can be used to not break implementors assuming that doesn’t break coherence.

Just throwing out an idea that adds a minimal amount of syntax to make the compatibility obvious: opt-in to the guarantee. (This could then be made automatic in the future if it’s found not to cause issues.)

trait SubTrait: super(SuperTrait) { .. }
3 Likes

I don’t like this whole thing, even tho I’m someone who likes inheritance sometimes.

Why not?

3 Likes

If super is not specified would we be blocked from doing this coercion?

That’s the idea; make it opt in such that it’s more obviously handled. (The specification could also scale to other cases where it needs to be specified, but that’s not the point currently.)

I prefer just coercions etc, and with any amount of supertypes. In fact, "any amount of supertypes" would be way more useful to me than this.

I think this idea has a lot of potential. However, I also think that this is by no means an obvious or simple change.

Extending the subtyping system is nontrivial, potentially affecting what guarantees unsafe code may rely on. Also, Rust has historically favoured composition over inheritance.

Therefore, even though I like the proposal, I think we should explore its benefits and drawbacks more carefully. I also think that the impact of this potential change warrants an RFC.

I am also in favour of making this opt-in. Because subtyping is subtle yet powerful, I think this behaviour should be explicit.

What guarentees about the layout of trait objects are there. I am not aware that there are any stable guarentees, so it should be fine to change it.

1 Like

Ok so just to be clear you are supposing a situation where you have:

  • a trait with one super-trait
  • the trait is object-safe (so it can be made dyn)
  • both the trait and super-trait are public (so that a third-party can make both dyn)
  • but a third, grand-super-trait is private (so that the third-party cannot implement them)
  • and a consumer of the crate is actually using both &dyn Trait and &dyn SuperTrait
  • and the consumer is relying on the fact that you can directly convert from one to the other
  • and the conversion from &dyn Trait to &dyn SuperTrait isn't explicitly intended
  • and you also realize that you suddenly want to make the trait have a second super-trait

Seems really niche to worry about

3 Likes

pub-in-private hacks are another way to have public traits that third parties can’t implement, and sealed traits may make this a first class feature.

Your syntax (which I think is better specified as an attribute trait T: #[super] K… though I don’t believe we allow attributes in type bound position right now) has another bonus: we can inherit from many non-marker traits, but the super trait can still be guaranteed to be the vtable prefix (this is similar to e.g. Scala where a class may inherit from only one class but from many traits).

2 Likes

How well does trait solving work in the face of subtyping? Neither Haskell or Rust has any true form of subtyping (lifetimes don’t count), and that’s the limits of my experience.

I’m thinking of things like:

// have some combination of
impl Trait for dyn Base { ... }
// and/or
impl Trait for dyn Derived { ... }

and how subtyping will play a role in searching for impls of Trait for dyn Derived.


I get the overall motivation for wanting to use subtyping; the coercions defined in rust are pretty limited and can’t reach deeply nested type parameters. The novelty just has me concerned!

1 Like